From b91627dbf3f62e362c4590b3768f5109cead4a65 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 26 Feb 2026 16:44:09 -0800 Subject: [PATCH 001/119] Fix text color --- src/QmlControls/VehicleSummaryRow.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/QmlControls/VehicleSummaryRow.qml b/src/QmlControls/VehicleSummaryRow.qml index 29c24b6d6c4a..533d00549cb3 100644 --- a/src/QmlControls/VehicleSummaryRow.qml +++ b/src/QmlControls/VehicleSummaryRow.qml @@ -2,6 +2,9 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QGroundControl +import QGroundControl.Controls + RowLayout { id: root @@ -17,7 +20,7 @@ RowLayout { } QGCLabel { text: root.valueText - color: root.valueColor !== "" ? root.valueColor : palette.text + color: root.valueColor !== "" ? root.valueColor : QGroundControl.globalPalette.text elide: Text.ElideRight horizontalAlignment: Text.AlignRight Layout.fillWidth: true From 0f04675d9ea1766eec3406456d24dcf3a493d3e9 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 27 Feb 2026 14:15:09 -0800 Subject: [PATCH 002/119] Back out concept of raw value not being set Was causing too many problems in random places which would only be identified if you stumbled across a dusty corner of the QGC qml ui. --- src/FactSystem/Fact.cc | 11 ----------- src/FactSystem/Fact.h | 1 - src/FactSystem/SettingsFact.cc | 1 - 3 files changed, 13 deletions(-) diff --git a/src/FactSystem/Fact.cc b/src/FactSystem/Fact.cc index 3044a6f6d391..35e661abfcc0 100644 --- a/src/FactSystem/Fact.cc +++ b/src/FactSystem/Fact.cc @@ -50,7 +50,6 @@ Fact::Fact(const QString& settingsGroup, FactMetaData *metaData, QObject *parent const QVariant defaultValue = metaData->rawDefaultValue(); QMutexLocker locker(&_rawValueMutex); _rawValue = defaultValue; - _rawValueIsNotSet = false; } } @@ -89,7 +88,6 @@ const Fact &Fact::operator=(const Fact& other) _name = other._name; _componentId = other._componentId; _rawValue = other._rawValue; - _rawValueIsNotSet = other._rawValueIsNotSet; _type = other._type; _sendValueChangedSignals = other._sendValueChangedSignals; _deferredValueChangeSignal = other._deferredValueChangeSignal; @@ -113,7 +111,6 @@ void Fact::forceSetRawValue(const QVariant &value) { QMutexLocker locker(&_rawValueMutex); _rawValue = typedValue; - _rawValueIsNotSet = false; } const QVariant cooked = _metaData->rawTranslator()(typedValue); @@ -139,7 +136,6 @@ void Fact::setRawValue(const QVariant &value) QMutexLocker locker(&_rawValueMutex); if (typedValue != _rawValue) { _rawValue = typedValue; - _rawValueIsNotSet = false; changed = true; } } @@ -201,7 +197,6 @@ void Fact::containerSetRawValue(const QVariant &value) QMutexLocker locker(&_rawValueMutex); if (_rawValue != value) { _rawValue = value; - _rawValueIsNotSet = false; changed = true; } currentRaw = _rawValue; @@ -357,12 +352,6 @@ QStringList Fact::selectedBitmaskStrings() const QString Fact::_variantToString(const QVariant &variant, int decimalPlaces) const { - QMutexLocker locker(&_rawValueMutex); - if (_rawValueIsNotSet) { - return invalidValueString(decimalPlaces); - } - locker.unlock(); - QString valueString; const auto stripNegativeZero = [](QString &candidate) { diff --git a/src/FactSystem/Fact.h b/src/FactSystem/Fact.h index 484ba722aa87..67598eb08f63 100644 --- a/src/FactSystem/Fact.h +++ b/src/FactSystem/Fact.h @@ -202,7 +202,6 @@ class Fact : public QObject QString _name; int _componentId = -1; QVariant _rawValue{0}; - bool _rawValueIsNotSet = true; mutable QRecursiveMutex _rawValueMutex; FactMetaData::ValueType_t _type = FactMetaData::valueTypeInt32; FactMetaData *_metaData = nullptr; diff --git a/src/FactSystem/SettingsFact.cc b/src/FactSystem/SettingsFact.cc index 5efffae1629a..c3152d807fec 100644 --- a/src/FactSystem/SettingsFact.cc +++ b/src/FactSystem/SettingsFact.cc @@ -49,7 +49,6 @@ SettingsFact::SettingsFact(const QString &settingsGroup, FactMetaData *metaData, QMutexLocker locker(&_rawValueMutex); _rawValue = resolvedValue; - _rawValueIsNotSet = false; } (void) connect(this, &Fact::rawValueChanged, this, &SettingsFact::_rawValueChanged); From e96b75eeadf5b67a45402fd0cda898a2145c67c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:48:31 +0000 Subject: [PATCH 003/119] Bump rollup in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [rollup](https://github.com/rollup/rollup). Updates `rollup` from 4.34.4 to 4.59.0 - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v4.34.4...v4.59.0) --- updated-dependencies: - dependency-name: rollup dependency-version: 4.59.0 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 483 ++++++++++++++++++++++++++++------------------ 1 file changed, 291 insertions(+), 192 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85045011f70a..8f4b89ba6203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -721,247 +721,300 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.4.tgz", - "integrity": "sha512-gGi5adZWvjtJU7Axs//CWaQbQd/vGy8KGcnEaCWiyCqxWYDxwIlAHFuSe6Guoxtd0SRvSfVTDMPd5H+4KE2kKA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.4.tgz", - "integrity": "sha512-1aRlh1gqtF7vNPMnlf1vJKk72Yshw5zknR/ZAVh7zycRAGF2XBMVDAHmFQz/Zws5k++nux3LOq/Ejj1WrDR6xg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.4.tgz", - "integrity": "sha512-drHl+4qhFj+PV/jrQ78p9ch6A0MfNVZScl/nBps5a7u01aGf/GuBRrHnRegA9bP222CBDfjYbFdjkIJ/FurvSQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.4.tgz", - "integrity": "sha512-hQqq/8QALU6t1+fbNmm6dwYsa0PDD4L5r3TpHx9dNl+aSEMnIksHZkSO3AVH+hBMvZhpumIGrTFj8XCOGuIXjw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.4.tgz", - "integrity": "sha512-/L0LixBmbefkec1JTeAQJP0ETzGjFtNml2gpQXA8rpLo7Md+iXQzo9kwEgzyat5Q+OG/C//2B9Fx52UxsOXbzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.4.tgz", - "integrity": "sha512-6Rk3PLRK+b8L/M6m/x6Mfj60LhAUcLJ34oPaxufA+CfqkUrDoUPQYFdRrhqyOvtOKXLJZJwxlOLbQjNYQcRQfw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.4.tgz", - "integrity": "sha512-kmT3x0IPRuXY/tNoABp2nDvI9EvdiS2JZsd4I9yOcLCCViKsP0gB38mVHOhluzx+SSVnM1KNn9k6osyXZhLoCA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.4.tgz", - "integrity": "sha512-3iSA9tx+4PZcJH/Wnwsvx/BY4qHpit/u2YoZoXugWVfc36/4mRkgGEoRbRV7nzNBSCOgbWMeuQ27IQWgJ7tRzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.4.tgz", - "integrity": "sha512-7CwSJW+sEhM9sESEk+pEREF2JL0BmyCro8UyTq0Kyh0nu1v0QPNY3yfLPFKChzVoUmaKj8zbdgBxUhBRR+xGxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.4.tgz", - "integrity": "sha512-GZdafB41/4s12j8Ss2izofjeFXRAAM7sHCb+S4JsI9vaONX/zQ8cXd87B9MRU/igGAJkKvmFmJJBeeT9jJ5Cbw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.4.tgz", - "integrity": "sha512-uuphLuw1X6ur11675c2twC6YxbzyLSpWggvdawTUamlsoUv81aAXRMPBC1uvQllnBGls0Qt5Siw8reSIBnbdqQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.4.tgz", - "integrity": "sha512-KvLEw1os2gSmD6k6QPCQMm2T9P2GYvsMZMRpMz78QpSoEevHbV/KOUbI/46/JRalhtSAYZBYLAnT9YE4i/l4vg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.4.tgz", - "integrity": "sha512-wcpCLHGM9yv+3Dql/CI4zrY2mpQ4WFergD3c9cpRowltEh5I84pRT/EuHZsG0In4eBPPYthXnuR++HrFkeqwkA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.4.tgz", - "integrity": "sha512-nLbfQp2lbJYU8obhRQusXKbuiqm4jSJteLwfjnunDT5ugBKdxqw1X9KWwk8xp1OMC6P5d0WbzxzhWoznuVK6XA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.4.tgz", - "integrity": "sha512-JGejzEfVzqc/XNiCKZj14eb6s5w8DdWlnQ5tWUbs99kkdvfq9btxxVX97AaxiUX7xJTKFA0LwoS0KU8C2faZRg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.4.tgz", - "integrity": "sha512-/iFIbhzeyZZy49ozAWJ1ZR2KW6ZdYUbQXLT4O5n1cRZRoTpwExnHLjlurDXXPKEGxiAg0ujaR9JDYKljpr2fDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.4.tgz", - "integrity": "sha512-qORc3UzoD5UUTneiP2Afg5n5Ti1GAW9Gp5vHPxzvAFFA3FBaum9WqGvYXGf+c7beFdOKNos31/41PRMUwh1tpA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.4.tgz", - "integrity": "sha512-5g7E2PHNK2uvoD5bASBD9aelm44nf1w4I5FEI7MPHLWcCSrR8JragXZWgKPXk5i2FU3JFfa6CGZLw2RrGBHs2Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], - "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.4.tgz", - "integrity": "sha512-p0scwGkR4kZ242xLPBuhSckrJ734frz6v9xZzD+kHVYRAkSUmdSLCIJRfql6H5//aF8Q10K+i7q8DiPfZp0b7A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -1047,10 +1100,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "license": "MIT" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "node_modules/@types/hast": { "version": "3.0.4", @@ -1878,12 +1930,11 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.4.tgz", - "integrity": "sha512-spF66xoyD7rz3o08sHP7wogp1gZ6itSq22SGa/IZTcUDXDlOyrShwMwkVSB+BUxFRZZCUYqdb3KWDEOMVQZxuw==", - "license": "MIT", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -1893,25 +1944,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.4", - "@rollup/rollup-android-arm64": "4.34.4", - "@rollup/rollup-darwin-arm64": "4.34.4", - "@rollup/rollup-darwin-x64": "4.34.4", - "@rollup/rollup-freebsd-arm64": "4.34.4", - "@rollup/rollup-freebsd-x64": "4.34.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.4", - "@rollup/rollup-linux-arm-musleabihf": "4.34.4", - "@rollup/rollup-linux-arm64-gnu": "4.34.4", - "@rollup/rollup-linux-arm64-musl": "4.34.4", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.4", - "@rollup/rollup-linux-riscv64-gnu": "4.34.4", - "@rollup/rollup-linux-s390x-gnu": "4.34.4", - "@rollup/rollup-linux-x64-gnu": "4.34.4", - "@rollup/rollup-linux-x64-musl": "4.34.4", - "@rollup/rollup-win32-arm64-msvc": "4.34.4", - "@rollup/rollup-win32-ia32-msvc": "4.34.4", - "@rollup/rollup-win32-x64-msvc": "4.34.4", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -2603,117 +2660,153 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "@rollup/rollup-android-arm-eabi": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.4.tgz", - "integrity": "sha512-gGi5adZWvjtJU7Axs//CWaQbQd/vGy8KGcnEaCWiyCqxWYDxwIlAHFuSe6Guoxtd0SRvSfVTDMPd5H+4KE2kKA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.4.tgz", - "integrity": "sha512-1aRlh1gqtF7vNPMnlf1vJKk72Yshw5zknR/ZAVh7zycRAGF2XBMVDAHmFQz/Zws5k++nux3LOq/Ejj1WrDR6xg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.4.tgz", - "integrity": "sha512-drHl+4qhFj+PV/jrQ78p9ch6A0MfNVZScl/nBps5a7u01aGf/GuBRrHnRegA9bP222CBDfjYbFdjkIJ/FurvSQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.4.tgz", - "integrity": "sha512-hQqq/8QALU6t1+fbNmm6dwYsa0PDD4L5r3TpHx9dNl+aSEMnIksHZkSO3AVH+hBMvZhpumIGrTFj8XCOGuIXjw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "optional": true }, "@rollup/rollup-freebsd-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.4.tgz", - "integrity": "sha512-/L0LixBmbefkec1JTeAQJP0ETzGjFtNml2gpQXA8rpLo7Md+iXQzo9kwEgzyat5Q+OG/C//2B9Fx52UxsOXbzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "optional": true }, "@rollup/rollup-freebsd-x64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.4.tgz", - "integrity": "sha512-6Rk3PLRK+b8L/M6m/x6Mfj60LhAUcLJ34oPaxufA+CfqkUrDoUPQYFdRrhqyOvtOKXLJZJwxlOLbQjNYQcRQfw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.4.tgz", - "integrity": "sha512-kmT3x0IPRuXY/tNoABp2nDvI9EvdiS2JZsd4I9yOcLCCViKsP0gB38mVHOhluzx+SSVnM1KNn9k6osyXZhLoCA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "optional": true }, "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.4.tgz", - "integrity": "sha512-3iSA9tx+4PZcJH/Wnwsvx/BY4qHpit/u2YoZoXugWVfc36/4mRkgGEoRbRV7nzNBSCOgbWMeuQ27IQWgJ7tRzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.4.tgz", - "integrity": "sha512-7CwSJW+sEhM9sESEk+pEREF2JL0BmyCro8UyTq0Kyh0nu1v0QPNY3yfLPFKChzVoUmaKj8zbdgBxUhBRR+xGxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.4.tgz", - "integrity": "sha512-GZdafB41/4s12j8Ss2izofjeFXRAAM7sHCb+S4JsI9vaONX/zQ8cXd87B9MRU/igGAJkKvmFmJJBeeT9jJ5Cbw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "optional": true + }, + "@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "optional": true }, - "@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.4.tgz", - "integrity": "sha512-uuphLuw1X6ur11675c2twC6YxbzyLSpWggvdawTUamlsoUv81aAXRMPBC1uvQllnBGls0Qt5Siw8reSIBnbdqQ==", + "@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "optional": true }, - "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.4.tgz", - "integrity": "sha512-KvLEw1os2gSmD6k6QPCQMm2T9P2GYvsMZMRpMz78QpSoEevHbV/KOUbI/46/JRalhtSAYZBYLAnT9YE4i/l4vg==", + "@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "optional": true + }, + "@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.4.tgz", - "integrity": "sha512-wcpCLHGM9yv+3Dql/CI4zrY2mpQ4WFergD3c9cpRowltEh5I84pRT/EuHZsG0In4eBPPYthXnuR++HrFkeqwkA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "optional": true }, "@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.4.tgz", - "integrity": "sha512-nLbfQp2lbJYU8obhRQusXKbuiqm4jSJteLwfjnunDT5ugBKdxqw1X9KWwk8xp1OMC6P5d0WbzxzhWoznuVK6XA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.4.tgz", - "integrity": "sha512-JGejzEfVzqc/XNiCKZj14eb6s5w8DdWlnQ5tWUbs99kkdvfq9btxxVX97AaxiUX7xJTKFA0LwoS0KU8C2faZRg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.4.tgz", - "integrity": "sha512-/iFIbhzeyZZy49ozAWJ1ZR2KW6ZdYUbQXLT4O5n1cRZRoTpwExnHLjlurDXXPKEGxiAg0ujaR9JDYKljpr2fDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "optional": true + }, + "@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "optional": true + }, + "@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.4.tgz", - "integrity": "sha512-qORc3UzoD5UUTneiP2Afg5n5Ti1GAW9Gp5vHPxzvAFFA3FBaum9WqGvYXGf+c7beFdOKNos31/41PRMUwh1tpA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.4.tgz", - "integrity": "sha512-5g7E2PHNK2uvoD5bASBD9aelm44nf1w4I5FEI7MPHLWcCSrR8JragXZWgKPXk5i2FU3JFfa6CGZLw2RrGBHs2Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "optional": true + }, + "@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.4.tgz", - "integrity": "sha512-p0scwGkR4kZ242xLPBuhSckrJ734frz6v9xZzD+kHVYRAkSUmdSLCIJRfql6H5//aF8Q10K+i7q8DiPfZp0b7A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "optional": true }, "@shikijs/core": { @@ -2788,9 +2881,9 @@ "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==" }, "@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "@types/hast": { "version": "3.0.4", @@ -3316,30 +3409,36 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "rollup": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.4.tgz", - "integrity": "sha512-spF66xoyD7rz3o08sHP7wogp1gZ6itSq22SGa/IZTcUDXDlOyrShwMwkVSB+BUxFRZZCUYqdb3KWDEOMVQZxuw==", - "requires": { - "@rollup/rollup-android-arm-eabi": "4.34.4", - "@rollup/rollup-android-arm64": "4.34.4", - "@rollup/rollup-darwin-arm64": "4.34.4", - "@rollup/rollup-darwin-x64": "4.34.4", - "@rollup/rollup-freebsd-arm64": "4.34.4", - "@rollup/rollup-freebsd-x64": "4.34.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.4", - "@rollup/rollup-linux-arm-musleabihf": "4.34.4", - "@rollup/rollup-linux-arm64-gnu": "4.34.4", - "@rollup/rollup-linux-arm64-musl": "4.34.4", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.4", - "@rollup/rollup-linux-riscv64-gnu": "4.34.4", - "@rollup/rollup-linux-s390x-gnu": "4.34.4", - "@rollup/rollup-linux-x64-gnu": "4.34.4", - "@rollup/rollup-linux-x64-musl": "4.34.4", - "@rollup/rollup-win32-arm64-msvc": "4.34.4", - "@rollup/rollup-win32-ia32-msvc": "4.34.4", - "@rollup/rollup-win32-x64-msvc": "4.34.4", - "@types/estree": "1.0.6", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "requires": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@types/estree": "1.0.8", "fsevents": "~2.3.2" } }, From e8664a25069a698f976eb2b14db0ca9d538ea3f1 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 28 Feb 2026 10:38:55 -0800 Subject: [PATCH 004/119] Vehicle: require COMMAND_ACK before advancing REQUEST_MESSAGE --- src/Vehicle/Vehicle.cc | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index cca1b13bf4ea..5c70fb52aa4d 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -3042,13 +3042,7 @@ void Vehicle::_waitForMavlinkMessageMessageReceivedHandler(const mavlink_message << queueDepth; if (!pInfo->commandAckReceived) { - qCDebug(VehicleLog) << "message received before ack came back."; - int entryIndex = _findMavCommandListEntryIndex(message.compid, MAV_CMD_REQUEST_MESSAGE); - if (entryIndex != -1) { - _mavCommandList.takeAt(entryIndex); - } else { - qWarning() << "Removing request message command from list failed - not found in list"; - } + return; } _removeRequestMessageInfo(message.compid, message.msgid); @@ -3137,13 +3131,8 @@ void Vehicle::_requestMessageCmdResultHandler(void* resultHandlerData_, [[maybe_ return; } - if (requestMessageInfo->messageReceived) { - // This should never happen. The command should have already been removed from the list when the message was received - qWarning() << "Command result handler should now have been called if message has already been received"; - } else { - // Now that the request has been acked we start the timer to wait for the message - requestMessageInfo->messageWaitElapsedTimer.start(); - } + // Now that the request has been acked we start the timer to wait for the message + requestMessageInfo->messageWaitElapsedTimer.start(); } void Vehicle::_requestMessageWaitForMessageResultHandler(void* resultHandlerData, bool noResponsefromVehicle, const mavlink_message_t& message) From 1db914ae2d05456204cb66abe93c1358119f0d50 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 28 Feb 2026 10:39:03 -0800 Subject: [PATCH 005/119] Vehicle: reduce default MAV_CMD ACK timeout to 1200ms --- src/Vehicle/Vehicle.cc | 25 ++++++++++++++++--------- src/Vehicle/Vehicle.h | 4 ++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index 5c70fb52aa4d..23e896b8b82a 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -2544,7 +2544,7 @@ int Vehicle::_mavCommandResponseCheckTimeoutMSecs() int Vehicle::_mavCommandAckTimeoutMSecs() { // Use shorter ack timeout during unit tests for faster test execution - return qgcApp()->runningUnitTests() ? kTestMavCommandAckTimeoutMs : 3000; + return qgcApp()->runningUnitTests() ? kTestMavCommandAckTimeoutMs : 1200; } bool Vehicle::_sendMavCommandShouldRetry(MAV_CMD command) @@ -3028,6 +3028,9 @@ void Vehicle::_waitForMavlinkMessageMessageReceivedHandler(const mavlink_message auto resultHandler = pInfo->resultHandler; auto resultHandlerData = pInfo->resultHandlerData; + pInfo->messageReceived = true; + pInfo->message = message; + const mavlink_message_info_t *info = mavlink_get_message_info_by_id(message.msgid); QString msgName = info ? QString(info->name) : QString::number(message.msgid); const int activeCount = _requestMessageInfoMap.contains(message.compid) ? _requestMessageInfoMap[message.compid].count() : 0; @@ -3041,12 +3044,10 @@ void Vehicle::_waitForMavlinkMessageMessageReceivedHandler(const mavlink_message << "queueDepth" << queueDepth; - if (!pInfo->commandAckReceived) { - return; + if (pInfo->commandAckReceived) { + _removeRequestMessageInfo(message.compid, message.msgid); + (*resultHandler)(resultHandlerData, MAV_RESULT_ACCEPTED, RequestMessageNoFailure, message); } - _removeRequestMessageInfo(message.compid, message.msgid); - - (*resultHandler)(resultHandlerData, MAV_RESULT_ACCEPTED, RequestMessageNoFailure, message); } else { // We use any incoming message as a trigger to check timeouts on message requests @@ -3087,7 +3088,6 @@ void Vehicle::_requestMessageCmdResultHandler(void* resultHandlerData_, [[maybe_ auto resultHandler = requestMessageInfo->resultHandler; auto resultHandlerData = requestMessageInfo->resultHandlerData; Vehicle* vehicle = requestMessageInfo->vehicle; // QPointer converts to raw pointer, null if Vehicle destroyed - auto message [[maybe_unused]] = requestMessageInfo->message; // Vehicle was destroyed before callback fired - clean up and return without accessing vehicle if (!vehicle) { @@ -3126,12 +3126,19 @@ void Vehicle::_requestMessageCmdResultHandler(void* resultHandlerData_, [[maybe_ vehicle->_removeRequestMessageInfo(requestMessageInfo->compId, requestMessageInfo->msgId); - (*resultHandler)(resultHandlerData, static_cast(ack.result), requestMessageFailureCode, ackMessage); + (*resultHandler)(resultHandlerData, static_cast(ack.result), requestMessageFailureCode, ackMessage); return; } - // Now that the request has been acked we start the timer to wait for the message + if (requestMessageInfo->messageReceived) { + auto message = requestMessageInfo->message; + vehicle->_removeRequestMessageInfo(requestMessageInfo->compId, requestMessageInfo->msgId); + (*resultHandler)(resultHandlerData, MAV_RESULT_ACCEPTED, RequestMessageNoFailure, message); + return; + } + + // We have the ack, but we are still waiting for the message. Start the timer to wait for the message requestMessageInfo->messageWaitElapsedTimer.start(); } diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index a2e3545368f3..3061791f93f2 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -1149,8 +1149,8 @@ void _activeVehicleChanged (Vehicle* newActiveVehicle); RequestMessageResultHandler resultHandler = nullptr; void* resultHandlerData = nullptr; bool commandAckReceived = false; // We keep track of the ack/message being received since the order in which this will come in is random - bool messageReceived = false; // We only delete the allocated RequestMessageInfo_t when both happen (or the message wait times out) - QElapsedTimer messageWaitElapsedTimer; // Elapsed time since we started waiting for the message to show up + bool messageReceived = false; // We only delete the allocated RequestMessageInfo_t when both the message is received and we get the ack + QElapsedTimer messageWaitElapsedTimer; // Elapsed time since we started waiting message to show up mavlink_message_t message; } RequestMessageInfo_t; From 95d91e75fa61ce57b668d5163259bd7cbfaae41c Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 27 Feb 2026 08:24:09 -0800 Subject: [PATCH 006/119] Request _HASH_CHECK before PARAM_REQUEST_LIST for PX4 For PX4 vehicles, send a standalone PARAM_REQUEST_READ for _HASH_CHECK before issuing PARAM_REQUEST_LIST. If the hash matches the local parameter cache, parameters are loaded from disk and the full parameter stream is skipped entirely, reducing reconnect time. Split the single _initialRequestTimeoutTimer into two independent timers: - _hashCheckTimer (1 s): guards the standalone _HASH_CHECK request - _paramRequestListTimer (5 s): guards PARAM_REQUEST_LIST (unchanged timeout) _hashCheckTimeout falls back to PARAM_REQUEST_LIST without consuming a retry. If _HASH_CHECK arrives later inside the PARAM_REQUEST_LIST stream the cache path still works. MockLink changes: - Compute a real CRC-32 hash over the parameter map so _HASH_CHECK responses match the ParameterManager cache logic, enabling cache-hit and cache-miss scenarios in tests. - Append _HASH_CHECK at the end of the PARAM_REQUEST_LIST stream (mirrors real PX4 firmware behaviour). - Add setHashCheckNoResponse() and setMockParamValue() test helpers. Co-Authored-By: Claude --- src/Comms/MockLink/MockLink.cc | 92 +++++++++++++++++++++++-- src/Comms/MockLink/MockLink.h | 14 +++- src/FactSystem/ParameterManager.cc | 106 ++++++++++++++++++++++------- src/FactSystem/ParameterManager.h | 10 ++- 4 files changed, 191 insertions(+), 31 deletions(-) diff --git a/src/Comms/MockLink/MockLink.cc b/src/Comms/MockLink/MockLink.cc index 16c2349c9c38..0ffddfbf97b1 100644 --- a/src/Comms/MockLink/MockLink.cc +++ b/src/Comms/MockLink/MockLink.cc @@ -7,6 +7,9 @@ #include "QGCApplication.h" #include "QGCLoggingCategory.h" #include "FirmwarePlugin.h" +#include "FactMetaData.h" +#include "ParameterManager.h" +#include "QGC.h" #include #include @@ -784,6 +787,13 @@ void MockLink::_setParamFloatUnionIntoMap(int componentId, const QString ¶mN _mapParamName2Value[componentId][paramName] = paramVariant; } +void MockLink::setMockParamValue(int componentId, const QString ¶mName, float value) +{ + mavlink_param_union_t valueUnion{}; + valueUnion.param_float = value; + _setParamFloatUnionIntoMap(componentId, paramName, valueUnion.param_float); +} + float MockLink::_floatUnionForParam(int componentId, const QString ¶mName) { Q_ASSERT(_mapParamName2Value.contains(componentId)); @@ -852,6 +862,37 @@ float MockLink::_floatUnionForParam(int componentId, const QString ¶mName) return valueUnion.param_float; } +uint32_t MockLink::_computeParamHash(int componentId) const +{ + // Volatile parameters are excluded from the hash, matching PX4 firmware and ParameterManager::_tryCacheHashLoad + static const QStringList volatileParams = { + QStringLiteral("COM_FLIGHT_UUID"), + QStringLiteral("EKF2_MAGBIAS_X"), + QStringLiteral("EKF2_MAGBIAS_Y"), + QStringLiteral("EKF2_MAGBIAS_Z"), + QStringLiteral("EKF2_MAG_DECL"), + QStringLiteral("LND_FLIGHT_T_HI"), + QStringLiteral("LND_FLIGHT_T_LO"), + QStringLiteral("SYS_RESTART_TYPE"), + }; + + uint32_t crc = 0; + const auto ¶ms = _mapParamName2Value[componentId]; + for (auto it = params.constBegin(); it != params.constEnd(); ++it) { + const QString &name = it.key(); + if (volatileParams.contains(name)) { + continue; + } + const QVariant &value = it.value(); + const MAV_PARAM_TYPE mavType = _mapParamName2MavParamType[componentId][name]; + const FactMetaData::ValueType_t factType = ParameterManager::mavTypeToFactType(mavType); + + crc = QGC::crc32(reinterpret_cast(qPrintable(name)), name.length(), crc); + crc = QGC::crc32(static_cast(value.constData()), FactMetaData::typeToSize(factType), crc); + } + return crc; +} + void MockLink::_handleParamRequestList(const mavlink_message_t &msg) { if (_failureMode == MockConfiguration::FailParamNoResponseToRequestList) { @@ -875,6 +916,7 @@ void MockLink::_handleParamRequestList(const mavlink_message_t &msg) // Start the worker routine _currentParamRequestListComponentIndex = 0; _currentParamRequestListParamIndex = 0; + _paramRequestListHashCheckSent = false; } void MockLink::_paramRequestListWorker() @@ -897,6 +939,37 @@ void MockLink::_paramRequestListWorker() const int cParameters = _paramRequestListParamNames.count(); if (_currentParamRequestListParamIndex >= cParameters) { + // All regular params sent — for PX4, append _HASH_CHECK as the last entry in the stream. + // Uses param_count=0, param_index=-1 (same as standalone response) so ParameterManager + // handles it via the _HASH_CHECK early-return path without affecting param count tracking. + if (_firmwareType == MAV_AUTOPILOT_PX4 && !_paramRequestListHashCheckSent) { + _paramRequestListHashCheckSent = true; + + mavlink_param_union_t valueUnion{}; + valueUnion.type = MAV_PARAM_TYPE_UINT32; + valueUnion.param_uint32 = _computeParamHash(componentId); + + char paramId[MAVLINK_MSG_ID_PARAM_VALUE_LEN]{}; + (void) strncpy(paramId, "_HASH_CHECK", MAVLINK_MSG_ID_PARAM_VALUE_LEN); + + qCDebug(MockLinkLog) << "Sending _HASH_CHECK in PARAM_REQUEST_LIST stream" << componentId << "hash:" << valueUnion.param_uint32; + + mavlink_message_t responseMsg{}; + (void) mavlink_msg_param_value_pack_chan( + _vehicleSystemId, + componentId, + mavlinkChannel(), + &responseMsg, + paramId, + valueUnion.param_float, + MAV_PARAM_TYPE_UINT32, + 0, // param_count: 0 to avoid affecting ParameterManager's count tracking + -1 // param_index: -1 signals this is a virtual/out-of-band parameter + ); + respondWithMavlinkMessage(responseMsg); + return; + } + // Move to next component if (++_currentParamRequestListComponentIndex >= _paramRequestListComponentIds.count()) { _currentParamRequestListComponentIndex = -1; @@ -906,6 +979,7 @@ void MockLink::_paramRequestListWorker() // Cache param names for the new component _paramRequestListParamNames = _mapParamName2Value[_paramRequestListComponentIds.at(_currentParamRequestListComponentIndex)].keys(); _currentParamRequestListParamIndex = 0; + _paramRequestListHashCheckSent = false; } return; } @@ -1005,15 +1079,23 @@ void MockLink::_handleParamRequestRead(const mavlink_message_t &msg) const QString paramName(QString::fromLocal8Bit(request.param_id, static_cast(strnlen(request.param_id, MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN)))); const int componentId = request.target_component; - // special case for magic _HASH_CHECK value - if ((request.target_component == MAV_COMP_ID_ALL) && (paramName == "_HASH_CHECK")) { + // special case for magic _HASH_CHECK value (PX4 only) + if ((_firmwareType == MAV_AUTOPILOT_PX4) && (paramName == "_HASH_CHECK")) { + _hashCheckRequestCount++; + if (_hashCheckNoResponse) { + return; + } + + const int hashComponentId = _mapParamName2Value.contains(MAV_COMP_ID_AUTOPILOT1) + ? MAV_COMP_ID_AUTOPILOT1 + : _mapParamName2Value.keys().first(); + mavlink_param_union_t valueUnion{}; valueUnion.type = MAV_PARAM_TYPE_UINT32; - valueUnion.param_uint32 = 0; - // Special case of magic hash check value + valueUnion.param_uint32 = _computeParamHash(hashComponentId); (void) mavlink_msg_param_value_pack_chan( _vehicleSystemId, - componentId, + hashComponentId, mavlinkChannel(), &responseMsg, request.param_id, diff --git a/src/Comms/MockLink/MockLink.h b/src/Comms/MockLink/MockLink.h index 6775137fdfe8..89ef26b69e10 100644 --- a/src/Comms/MockLink/MockLink.h +++ b/src/Comms/MockLink/MockLink.h @@ -84,7 +84,7 @@ class MockLink : public LinkInterface int receivedRequestMessageCount(int compId, int messageId) const { return _receivedRequestMessageByCompAndMsgCountMap.value(compId).value(messageId, 0); } void clearReceivedRequestMessageCounts() { _receivedRequestMessageCountMap.clear(); _receivedRequestMessageByCompAndMsgCountMap.clear(); } int receivedRequestMessageCount(uint32_t messageId) const { return _receivedRequestMessageCountMap.value(messageId, 0); } - void clearReceivedMavlinkMessageCounts() { _receivedMavlinkMessageCountMap.clear(); } + void clearReceivedMavlinkMessageCounts() { _receivedMavlinkMessageCountMap.clear(); _hashCheckRequestCount = 0; } int receivedMavlinkMessageCount(uint32_t messageId) const { return _receivedMavlinkMessageCountMap.value(messageId, 0); } enum RequestMessageFailureMode_t { @@ -115,6 +115,14 @@ class MockLink : public LinkInterface _paramRequestReadFailureFirstAttemptPending = (mode == FailParamRequestReadFirstAttemptNoResponse); } + void setHashCheckNoResponse(bool noResponse) { _hashCheckNoResponse = noResponse; } + + /// Returns the number of standalone PARAM_REQUEST_READ requests for _HASH_CHECK received + int hashCheckRequestCount() const { return _hashCheckRequestCount; } + + /// Change a float parameter value directly on MockLink (for testing cache invalidation) + void setMockParamValue(int componentId, const QString ¶mName, float value); + static MockLink *startPX4MockLink(bool sendStatusText, bool enableCamera, bool enableGimbal, MockConfiguration::FailureMode_t failureMode = MockConfiguration::FailNone); static MockLink *startGenericMockLink(bool sendStatusText, bool enableCamera, bool enableGimbal, MockConfiguration::FailureMode_t failureMode = MockConfiguration::FailNone); static MockLink *startNoInitialConnectMockLink(bool sendStatusText, bool enableCamera, bool enableGimbal, MockConfiguration::FailureMode_t failureMode = MockConfiguration::FailNone); @@ -163,6 +171,7 @@ private slots: /// Convert from a parameter variant to the float value from mavlink_param_union_t float _floatUnionForParam(int componentId, const QString ¶mName); + uint32_t _computeParamHash(int componentId) const; void _setParamFloatUnionIntoMap(int componentId, const QString ¶mName, float paramFloat); /// Handle incoming bytes which are meant to be interpreted by the NuttX shell @@ -303,6 +312,9 @@ private slots: bool _paramSetFailureFirstAttemptPending = false; ParamRequestReadFailureMode_t _paramRequestReadFailureMode = FailParamRequestReadNone; bool _paramRequestReadFailureFirstAttemptPending = false; + bool _hashCheckNoResponse = false; + int _hashCheckRequestCount = 0; + bool _paramRequestListHashCheckSent = false; QMap _receivedMavCommandCountMap; QMap> _receivedMavCommandByCompCountMap; diff --git a/src/FactSystem/ParameterManager.cc b/src/FactSystem/ParameterManager.cc index 5878c59a4398..6a845f298361 100644 --- a/src/FactSystem/ParameterManager.cc +++ b/src/FactSystem/ParameterManager.cc @@ -41,10 +41,13 @@ ParameterManager::ParameterManager(Vehicle *vehicle) qCDebug(ParameterManagerLog) << this << "In log replay mode"; } - _initialRequestTimeoutTimer.setSingleShot(true); - // Use much shorter timeouts in unit tests since MockLink responds instantly - _initialRequestTimeoutTimer.setInterval(qgcApp()->runningUnitTests() ? 500 : 5000); - (void) connect(&_initialRequestTimeoutTimer, &QTimer::timeout, this, &ParameterManager::_initialRequestTimeout); + _hashCheckTimer.setSingleShot(true); + _hashCheckTimer.setInterval(kHashCheckTimeoutMs); + (void) connect(&_hashCheckTimer, &QTimer::timeout, this, &ParameterManager::_hashCheckTimeout); + + _paramRequestListTimer.setSingleShot(true); + _paramRequestListTimer.setInterval(qgcApp()->runningUnitTests() ? kTestInitialRequestIntervalMs : kParamRequestListTimeoutMs); + (void) connect(&_paramRequestListTimer, &QTimer::timeout, this, &ParameterManager::_paramRequestListTimeout); _waitingParamTimeoutTimer.setSingleShot(true); _waitingParamTimeoutTimer.setInterval(qgcApp()->runningUnitTests() ? 500 : 3000); @@ -123,14 +126,13 @@ void ParameterManager::_handleParamValue(int componentId, const QString ¶met // ArduPilot has this strange behavior of streaming parameters that we didn't ask for. This even happens before it responds to the // PARAM_REQUEST_LIST. We disregard any of this until the initial request is responded to. - if ((parameterIndex == 65535) && (parameterName != QStringLiteral("_HASH_CHECK")) && _initialRequestTimeoutTimer.isActive()) { + if ((parameterIndex == 65535) && (parameterName != QStringLiteral("_HASH_CHECK")) && _paramRequestListTimer.isActive()) { qCDebug(ParameterManagerLog) << "Disregarding unrequested param prior to initial list response" << parameterName; return; } - _initialRequestTimeoutTimer.stop(); - if (_vehicle->px4Firmware() && (parameterName == "_HASH_CHECK")) { + _hashCheckTimer.stop(); if (!_initialLoadComplete && !_logReplay) { /* we received a cache hash, potentially load from cache */ _tryCacheHashLoad(_vehicle->id(), componentId, parameterValue); @@ -138,6 +140,8 @@ void ParameterManager::_handleParamValue(int componentId, const QString ¶met return; } + _paramRequestListTimer.stop(); + // Used to debug cache crc misses (turn on ParameterManagerDebugCacheFailureLog) if (!_initialLoadComplete && !_logReplay && _debugCacheCRC.contains(componentId) && _debugCacheCRC[componentId]) { if (_debugCacheMap[componentId].contains(parameterName)) { @@ -155,7 +159,6 @@ void ParameterManager::_handleParamValue(int componentId, const QString ¶met } } - _initialRequestTimeoutTimer.stop(); _waitingParamTimeoutTimer.stop(); // Update our total parameter counts @@ -521,12 +524,12 @@ void ParameterManager::_ftpDownloadComplete(const QString &fileName, const QStri * can immediately response with the conventional parameter download request, because * we have no indication of communication link congestion.*/ if (immediateRetry) { - _initialRequestTimeout(); + _paramRequestListTimeout(); } else { - _initialRequestTimeoutTimer.start(); + _paramRequestListTimer.start(); } } else { - _initialRequestTimeoutTimer.start(); + _paramRequestListTimer.start(); } } @@ -535,7 +538,7 @@ void ParameterManager::_ftpDownloadProgress(float progress) qCDebug(ParameterManagerVerbose1Log) << "ParameterManager::_ftpDownloadProgress:" << progress; _setLoadProgress(static_cast(progress)); if (progress > 0.001) { - _initialRequestTimeoutTimer.stop(); + _paramRequestListTimer.stop(); } } @@ -556,11 +559,10 @@ void ParameterManager::refreshAllParameters(uint8_t componentId) emit missingParametersChanged(_missingParameters); } - if (!_initialLoadComplete) { - _initialRequestTimeoutTimer.start(); - } - if (_tryftp && ((componentId == MAV_COMP_ID_ALL) || (componentId == MAV_COMP_ID_AUTOPILOT1))) { + if (!_initialLoadComplete) { + _paramRequestListTimer.start(); + } FTPManager *const ftpManager = _vehicle->ftpManager(); (void) connect(ftpManager, &FTPManager::downloadComplete, this, &ParameterManager::_ftpDownloadComplete); _waitingParamTimeoutTimer.stop(); @@ -574,7 +576,19 @@ void ParameterManager::refreshAllParameters(uint8_t componentId) qCWarning(ParameterManagerLog) << "ParameterManager::refreshallParameters FTPManager::download returned failure"; (void) disconnect(ftpManager, &FTPManager::downloadComplete, this, &ParameterManager::_ftpDownloadComplete); } + } else if (_vehicle->px4Firmware() && !_initialLoadComplete && !_hashCheckDone) { + // PX4: Try _HASH_CHECK first to see if we can load from cache without a full parameter stream + _hashCheckTimer.start(); + qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "Requesting _HASH_CHECK before full parameter list"; + const uint8_t hashCheckCompId = (componentId == MAV_COMP_ID_ALL) + ? static_cast(MAV_COMP_ID_AUTOPILOT1) + : componentId; + _requestHashCheck(hashCheckCompId); } else { + if (!_initialLoadComplete) { + _paramRequestListTimer.start(); + } + // Reset index wait lists for (int cid: _paramCountMap.keys()) { // Add/Update all indices to the wait list, parameter index is 0-based @@ -753,6 +767,32 @@ void ParameterManager::_waitingParamTimeout() } } +void ParameterManager::_requestHashCheck(uint8_t componentId) +{ + const SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); + if (!sharedLink) { + return; + } + + qCDebug(ParameterManagerLog) << _logVehiclePrefix(componentId) << "Sending PARAM_REQUEST_READ for _HASH_CHECK"; + + char paramId[MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN + 1] = {}; + (void) strncpy(paramId, "_HASH_CHECK", MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN); + + mavlink_message_t msg{}; + (void) mavlink_msg_param_request_read_pack_chan( + MAVLinkProtocol::instance()->getSystemId(), + MAVLinkProtocol::getComponentId(), + sharedLink->mavlinkChannel(), + &msg, + static_cast(_vehicle->id()), + componentId, + paramId, + -1); + + (void) _vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg); +} + void ParameterManager::_mavlinkParamRequestRead(int componentId, const QString ¶mName, int paramIndex, bool notifyFailure) { auto paramRequestReadEncoder = [this, componentId, paramName, paramIndex](uint8_t /*systemId*/, uint8_t channel, mavlink_message_t *message) -> void { @@ -881,13 +921,19 @@ QString ParameterManager::parameterCacheFile(int vehicleId, int componentId) void ParameterManager::_tryCacheHashLoad(int vehicleId, int componentId, const QVariant &hashValue) { - qCInfo(ParameterManagerLog) << "Attemping load from cache"; + qCDebug(ParameterManagerLog) << "Attemping load from cache"; /* The datastructure of the cache table */ CacheMapName2ParamTypeVal cacheMap; QFile cacheFile(parameterCacheFile(vehicleId, componentId)); if (!cacheFile.exists()) { - /* no local cache, just wait for them to come in*/ + qCDebug(ParameterManagerLog) << "No parameter cache file"; + if (!_hashCheckDone) { + // Standalone hash check path — fall back to PARAM_REQUEST_LIST + _hashCheckDone = true; + refreshAllParameters(); + } + // If already in PARAM_REQUEST_LIST flow, just let the stream continue return; } (void) cacheFile.open(QIODevice::ReadOnly); @@ -915,7 +961,9 @@ void ParameterManager::_tryCacheHashLoad(int vehicleId, int componentId, const Q /* if the two param set hashes match, just load from the disk */ if (crc32_value == hashValue.toUInt()) { - qCInfo(ParameterManagerLog) << "Parameters loaded from cache" << qPrintable(QFileInfo(cacheFile).absoluteFilePath()); + _hashCheckDone = true; + _paramRequestListTimer.stop(); + qCDebug(ParameterManagerLog) << "Parameters loaded from cache" << qPrintable(QFileInfo(cacheFile).absoluteFilePath()); const int count = cacheMap.count(); int index = 0; @@ -968,7 +1016,7 @@ void ParameterManager::_tryCacheHashLoad(int vehicleId, int componentId, const Q ani->start(QAbstractAnimation::DeleteWhenStopped); } else { - qCInfo(ParameterManagerLog) << "Parameters cache match failed" << qPrintable(QFileInfo(cacheFile).absoluteFilePath()); + qCDebug(ParameterManagerLog) << "Parameters cache match failed" << qPrintable(QFileInfo(cacheFile).absoluteFilePath()); if (ParameterManagerDebugCacheFailureLog().isDebugEnabled()) { _debugCacheCRC[componentId] = true; _debugCacheMap[componentId] = cacheMap; @@ -977,6 +1025,12 @@ void ParameterManager::_tryCacheHashLoad(int vehicleId, int componentId, const Q } qgcApp()->showAppMessage(tr("Parameter cache CRC match failed")); } + if (!_hashCheckDone) { + // Standalone hash check path — fall back to PARAM_REQUEST_LIST + _hashCheckDone = true; + refreshAllParameters(); + } + // If already in PARAM_REQUEST_LIST flow, just let the stream continue } } @@ -1195,11 +1249,18 @@ void ParameterManager::_checkInitialLoadComplete() emit missingParametersChanged(_missingParameters); } -void ParameterManager::_initialRequestTimeout() +void ParameterManager::_hashCheckTimeout() +{ + _hashCheckDone = true; + qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "_HASH_CHECK timed out, falling back to PARAM_REQUEST_LIST"; + refreshAllParameters(); +} + +void ParameterManager::_paramRequestListTimeout() { if (_logReplay) { // Signal load complete - qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "_initialRequestTimeout (log replay): Signalling load complete"; + qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "_paramRequestListTimeout (log replay): Signalling load complete"; _initialLoadComplete = true; _missingParameters = false; _parametersReady = true; @@ -1212,7 +1273,6 @@ void ParameterManager::_initialRequestTimeout() if (!_disableAllRetries && (++_initialRequestRetryCount <= _maxInitialRequestListRetry)) { qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "Retrying initial parameter request list"; refreshAllParameters(); - _initialRequestTimeoutTimer.start(); } else if (!_vehicle->genericFirmware()) { const QString errorMsg = tr("Vehicle %1 did not respond to request for parameters. " "This will cause %2 to be unable to display its full user interface.").arg(_vehicle->id()).arg(QCoreApplication::applicationName()); diff --git a/src/FactSystem/ParameterManager.h b/src/FactSystem/ParameterManager.h index 34c3d9546ba0..ff47303a4d08 100644 --- a/src/FactSystem/ParameterManager.h +++ b/src/FactSystem/ParameterManager.h @@ -94,6 +94,8 @@ class ParameterManager : public QObject static constexpr int kParamRequestReadRetryCount = 2; ///< Number of retries for PARAM_REQUEST_READ static constexpr int kWaitForParamValueAckMs = 1000; ///< Time to wait for param value ack after set param static constexpr int kMaxInitialRequestListRetry = 4; ///< Maximum retries for initial parameter request list + static constexpr int kHashCheckTimeoutMs = 1000; ///< Timeout for standalone _HASH_CHECK request + static constexpr int kParamRequestListTimeoutMs = 5000; ///< Timeout for PARAM_REQUEST_LIST response static constexpr int kTestInitialRequestIntervalMs = 500; ///< Timer interval for initial request in test mode /// Maximum time to wait for initial request retries to exhaust in tests static constexpr int kTestMaxInitialRequestTimeMs = (kMaxInitialRequestListRetry + 1) * kTestInitialRequestIntervalMs + 1000; @@ -121,10 +123,12 @@ private slots: void _mavlinkParamSet(int componentId, const QString &name, FactMetaData::ValueType_t valueType, const QVariant &rawValue); void _waitingParamTimeout(); void _tryCacheLookup(); - void _initialRequestTimeout(); + void _hashCheckTimeout(); + void _paramRequestListTimeout(); /// Translates ParameterManager::defaultComponentId to real component id if needed int _actualComponentId(int componentId) const; void _mavlinkParamRequestRead(int componentId, const QString ¶mName, int paramIndex, bool notifyFailure); + void _requestHashCheck(uint8_t componentId); void _writeLocalParamCache(int vehicleId, int componentId); void _tryCacheHashLoad(int vehicleId, int componentId, const QVariant &hashValue); void _loadMetaData(); @@ -165,6 +169,7 @@ private slots: bool _waitingForDefaultComponent = false; ///< true: last chance wait for default component params bool _metaDataAddedToFacts = false; ///< true: FactMetaData has been adde to the default component facts bool _logReplay = false; ///< true: running with log replay link + bool _hashCheckDone = false; ///< true: _HASH_CHECK has been attempted, go straight to PARAM_REQUEST_LIST typedef QPair ParamTypeVal; typedef QMap CacheMapName2ParamTypeVal; @@ -193,7 +198,8 @@ private slots: int _totalParamCount = 0; ///< Number of parameters across all components int _pendingWritesCount = 0; ///< Number of parameters with pending writes - QTimer _initialRequestTimeoutTimer; + QTimer _hashCheckTimer; + QTimer _paramRequestListTimer; QTimer _waitingParamTimeoutTimer; Fact _defaultFact; ///< Used to return default fact, when parameter not found From f830bce07afcc2ece70601cd455b192af018b7fd Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 27 Feb 2026 08:24:14 -0800 Subject: [PATCH 007/119] Add HashCheckTest for _HASH_CHECK parameter cache scenarios Data-driven test matrix covering 12 scenarios: - FirstConnect_NoCache, Reconnect_CacheHit, Reconnect_CacheMiss - HashTimeout with and without cache, HashTimeout with stale cache - BothTimersExhaust (params never ready) - CacheDeleted_Between, ManualRefresh - ArduPilot (FTP path, no _HASH_CHECK), HighLatency, LogReplay Each row specifies MockLink configuration flags and expected outcomes (hash-check sent, PARAM_REQUEST_LIST sent, parametersReady, missingParameters). Co-Authored-By: Claude --- src/Comms/MockLink/MockLink.cc | 12 ++ test/CMakeLists.txt | 1 + test/FactSystem/CMakeLists.txt | 2 + test/FactSystem/HashCheckTest.cc | 267 +++++++++++++++++++++++++++++++ test/FactSystem/HashCheckTest.h | 23 +++ 5 files changed, 305 insertions(+) create mode 100644 test/FactSystem/HashCheckTest.cc create mode 100644 test/FactSystem/HashCheckTest.h diff --git a/src/Comms/MockLink/MockLink.cc b/src/Comms/MockLink/MockLink.cc index 0ffddfbf97b1..8726262e4bc4 100644 --- a/src/Comms/MockLink/MockLink.cc +++ b/src/Comms/MockLink/MockLink.cc @@ -1035,6 +1035,18 @@ void MockLink::_handleParamSet(const mavlink_message_t &msg) qCDebug(MockLinkLog) << "_handleParamSet" << componentId << paramId << request.param_type; + // PX4 special case: _HASH_CHECK is a virtual parameter used by ParameterManager + // to signal cache-hit and stop parameter streaming. It is intentionally not part + // of the normal parameter maps. + if ((_firmwareType == MAV_AUTOPILOT_PX4) && (strncmp(paramId, "_HASH_CHECK", MAVLINK_MSG_PARAM_SET_FIELD_PARAM_ID_LEN) == 0)) { + QMutexLocker locker(&_paramRequestListMutex); + _currentParamRequestListComponentIndex = -1; + _paramRequestListComponentIds.clear(); + _paramRequestListParamNames.clear(); + qCDebug(MockLinkLog) << "Received _HASH_CHECK PARAM_SET, stopping parameter stream"; + return; + } + Q_ASSERT(_mapParamName2Value.contains(componentId)); Q_ASSERT(_mapParamName2MavParamType.contains(componentId)); Q_ASSERT(_mapParamName2Value[componentId].contains(paramId)); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e4ec6e545b33..445025bda190 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -105,6 +105,7 @@ add_qgc_test(FactSystemTestGeneric LABELS Integration Vehicle RESOURCE_LOCK Mock add_qgc_test(FactSystemTestPX4 LABELS Integration Vehicle RESOURCE_LOCK MockLink) add_qgc_test(FactTest LABELS Unit) add_qgc_test(FactValueSliderListModelTest LABELS Unit) +add_qgc_test(HashCheckTest LABELS Integration Vehicle SERIAL TIMEOUT ${QGC_TEST_TIMEOUT_EXTENDED}) add_qgc_test(ParameterManagerTest LABELS Integration Vehicle SERIAL TIMEOUT ${QGC_TEST_TIMEOUT_EXTENDED}) # ---------------------------------------------------------------------------- diff --git a/test/FactSystem/CMakeLists.txt b/test/FactSystem/CMakeLists.txt index fc01dc8e5931..f98af7a00e6d 100644 --- a/test/FactSystem/CMakeLists.txt +++ b/test/FactSystem/CMakeLists.txt @@ -19,6 +19,8 @@ target_sources(${CMAKE_PROJECT_NAME} FactTest.h FactValueSliderListModelTest.cc FactValueSliderListModelTest.h + HashCheckTest.cc + HashCheckTest.h ParameterManagerTest.cc ParameterManagerTest.h ) diff --git a/test/FactSystem/HashCheckTest.cc b/test/FactSystem/HashCheckTest.cc new file mode 100644 index 000000000000..fbce21b7765a --- /dev/null +++ b/test/FactSystem/HashCheckTest.cc @@ -0,0 +1,267 @@ +#include "HashCheckTest.h" + +#include +#include +#include +#include + +#include "LinkManager.h" +#include "MAVLinkLib.h" +#include "MockConfiguration.h" +#include "MultiVehicleManager.h" +#include "ParameterManager.h" +#include "Vehicle.h" + +void HashCheckTest::cleanup() +{ + if (_mockLink && MultiVehicleManager::instance()->activeVehicle()) { + QSignalSpy spy(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged); + _mockLink->disconnect(); + _mockLink = nullptr; + (void) UnitTest::waitForSignal(spy, TestTimeout::mediumMs(), QStringLiteral("activeVehicleChanged")); + } + VehicleTestManualConnect::cleanup(); +} + +void HashCheckTest::_deleteCacheFiles() +{ + const QDir cacheDir = ParameterManager::parameterCacheDir(); + if (cacheDir.exists()) { + const QStringList cacheFiles = cacheDir.entryList(QStringList() << QStringLiteral("*.v2"), QDir::Files); + for (const QString &file : cacheFiles) { + QFile::remove(cacheDir.filePath(file)); + } + } +} + +void HashCheckTest::_connectAndWaitForParams() +{ + MultiVehicleManager *const vehicleMgr = MultiVehicleManager::instance(); + QVERIFY(vehicleMgr); + + QSignalSpy spyVehicle(vehicleMgr, &MultiVehicleManager::activeVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyVehicle, TestTimeout::mediumMs()); + + Vehicle *const vehicle = vehicleMgr->activeVehicle(); + QVERIFY(vehicle); + + QSignalSpy spyParamsReady(vehicleMgr, &MultiVehicleManager::parameterReadyVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyParamsReady, TestTimeout::longMs()); + + const QList arguments = spyParamsReady.takeFirst(); + QCOMPARE(arguments.count(), 1); + QCOMPARE(arguments.at(0).toBool(), true); +} + +void HashCheckTest::_disconnectAndSettle() +{ + _mockLink->disconnect(); + _mockLink = nullptr; + QSignalSpy spyDisconnect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged); + QVERIFY(UnitTest::waitForSignal(spyDisconnect, TestTimeout::longMs(), QStringLiteral("activeVehicleChanged"))); + UnitTest::settleEventLoopForCleanup(); +} + +MockLink *HashCheckTest::_startPX4MockLinkNoIncrement(MockConfiguration::FailureMode_t failureMode) +{ + auto *const mockConfig = new MockConfiguration(QStringLiteral("PX4 MockLink")); + mockConfig->setFirmwareType(MAV_AUTOPILOT_PX4); + mockConfig->setVehicleType(MAV_TYPE_QUADROTOR); + mockConfig->setIncrementVehicleId(false); + mockConfig->setFailureMode(failureMode); + mockConfig->setDynamic(true); + + SharedLinkConfigurationPtr config = LinkManager::instance()->addConfiguration(mockConfig); + if (LinkManager::instance()->createConnectedLink(config)) { + return qobject_cast(config->link()); + } + return nullptr; +} + +MockLink *HashCheckTest::_startPX4MockLinkHighLatency() +{ + auto *const mockConfig = new MockConfiguration(QStringLiteral("PX4 HighLatency MockLink")); + mockConfig->setFirmwareType(MAV_AUTOPILOT_PX4); + mockConfig->setVehicleType(MAV_TYPE_QUADROTOR); + mockConfig->setHighLatency(true); + mockConfig->setDynamic(true); + + SharedLinkConfigurationPtr config = LinkManager::instance()->addConfiguration(mockConfig); + if (LinkManager::instance()->createConnectedLink(config)) { + return qobject_cast(config->link()); + } + return nullptr; +} + +// Data-driven test matrix for all _HASH_CHECK parameter cache scenarios. +// +// FirstConnect_NoCache: First PX4 connection with no cache. Sends _HASH_CHECK, gets full param list. +// Reconnect_CacheHit: Reconnect after populating cache. Hash matches so no PARAM_REQUEST_LIST needed. +// Reconnect_CacheMiss: Reconnect after a parameter changed. Hash mismatch triggers full param reload. +// HashTimeout_CacheHit: Vehicle never responds to _HASH_CHECK. Falls back to PARAM_REQUEST_LIST, cache still valid. +// HashTimeout_NoCache: Vehicle never responds to _HASH_CHECK and no cache exists. Falls back to full param list. +// HashTimeout_CacheStale: Vehicle never responds to _HASH_CHECK and cache is stale. Falls back to full param list. +// BothTimersExhaust: Vehicle responds to neither _HASH_CHECK nor PARAM_REQUEST_LIST. Params never become ready. +// CacheDeleted_Between: Cache populated then deleted before reconnect. Forces full param reload despite same vehicle. +// ManualRefresh: User-triggered refreshAllParameters() bypasses _HASH_CHECK and requests full param list. +// ArduPilot: ArduPilot uses FTP for parameters, so no _HASH_CHECK or PARAM_REQUEST_LIST traffic. +// HighLatency: High-latency links skip parameter download entirely; params marked as missing. +// LogReplay: Log replay shares the high-latency code path; params marked as missing. +// +// PARAM_REQUEST_LIST (xPRL) is only asserted for PX4 vehicles; ArduPilot uses FTP. +// missingParameters (xMiss) is only asserted when expectParametersReady (xReady) is true. +// Scenarios 11 and 12 exercise the same code path via setHighLatency(true). +// MockLink doesn't support isLogReplay(), so high latency serves as proxy for the +// shared guard: if (isHighLatency || _logReplay) { signal ready immediately }. + +void HashCheckTest::_hashCheckMatrix_data() +{ + // MockLink configuration flags + QTest::addColumn("px4"); + QTest::addColumn("highLatency"); + QTest::addColumn("populateCache"); + QTest::addColumn("changeParam"); + QTest::addColumn("deleteCache"); + QTest::addColumn("hashCheckNoResponse"); + QTest::addColumn("failNoResponse"); + QTest::addColumn("manualRefresh"); + + // Expected outcomes + QTest::addColumn("xHashCheck"); + QTest::addColumn("xPRL"); + QTest::addColumn("xReady"); + QTest::addColumn("xMissing"); + + // px4 hl popC chgP delC noResp failNR manRef xHC xPRL xReady xMiss + QTest::newRow("FirstConnect_NoCache") << true << false << false << false << false << false << false << false << true << true << true << false; + QTest::newRow("Reconnect_CacheHit") << true << false << true << false << false << false << false << false << true << false << true << false; + QTest::newRow("Reconnect_CacheMiss") << true << false << true << true << false << false << false << false << true << true << true << false; + QTest::newRow("HashTimeout_CacheHit") << true << false << true << false << false << true << false << false << true << true << true << false; + QTest::newRow("HashTimeout_NoCache") << true << false << false << false << false << true << false << false << true << true << true << false; + QTest::newRow("HashTimeout_CacheStale") << true << false << true << true << false << true << false << false << true << true << true << false; + QTest::newRow("BothTimersExhaust") << true << false << false << false << false << true << true << false << true << true << false << false; + QTest::newRow("CacheDeleted_Between") << true << false << true << false << true << false << false << false << true << true << true << false; + QTest::newRow("ManualRefresh") << true << false << false << false << false << false << false << true << false << true << true << false; + QTest::newRow("ArduPilot") << false << false << false << false << false << false << false << false << false << false << true << false; + QTest::newRow("HighLatency") << true << true << false << false << false << false << false << false << false << false << true << true; + QTest::newRow("LogReplay") << true << true << false << false << false << false << false << false << false << false << true << true; +} + +void HashCheckTest::_hashCheckMatrix() +{ + QFETCH(bool, px4); + QFETCH(bool, highLatency); + QFETCH(bool, populateCache); + QFETCH(bool, changeParam); + QFETCH(bool, deleteCache); + QFETCH(bool, hashCheckNoResponse); + QFETCH(bool, failNoResponse); + QFETCH(bool, manualRefresh); + QFETCH(bool, xHashCheck); + QFETCH(bool, xPRL); + QFETCH(bool, xReady); + QFETCH(bool, xMissing); + + const MAV_AUTOPILOT firmwareType = px4 ? MAV_AUTOPILOT_PX4 : MAV_AUTOPILOT_ARDUPILOTMEGA; + const auto failMode = failNoResponse ? MockConfiguration::FailParamNoResponseToRequestList + : MockConfiguration::FailNone; + + // Phase 1: Clean slate + _deleteCacheFiles(); + + // Phase 2: Optional preconnect to populate the parameter cache + if (populateCache) { + _mockLink = _startPX4MockLinkNoIncrement(); + _connectAndWaitForParams(); + _disconnectAndSettle(); + + if (deleteCache) { + _deleteCacheFiles(); + } + } + + // Phase 3: Create the test link and connect + if (manualRefresh) { + _connectMockLink(firmwareType); + QVERIFY(_vehicle); + QVERIFY(_vehicle->parameterManager()->parametersReady()); + + _mockLink->clearReceivedMavlinkMessageCounts(); + _vehicle->parameterManager()->refreshAllParameters(); + QTest::qWait(100); + + } else if (highLatency) { + _mockLink = _startPX4MockLinkHighLatency(); + + MultiVehicleManager *const vehicleMgr = MultiVehicleManager::instance(); + QVERIFY(vehicleMgr); + + QSignalSpy spyVehicle(vehicleMgr, &MultiVehicleManager::activeVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyVehicle, TestTimeout::mediumMs()); + + Vehicle *const vehicle = vehicleMgr->activeVehicle(); + QVERIFY(vehicle); + + QSignalSpy spyParamsReady(vehicleMgr, &MultiVehicleManager::parameterReadyVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyParamsReady, TestTimeout::longMs()); + + } else if (!px4) { + _connectMockLink(firmwareType); + QVERIFY(_vehicle); + + } else if (xReady) { + // PX4 normal path — params will become ready + _mockLink = _startPX4MockLinkNoIncrement(failMode); + if (changeParam) { + _mockLink->setMockParamValue(MAV_COMP_ID_AUTOPILOT1, QStringLiteral("BAT1_V_CHARGED"), 99.0f); + } + if (hashCheckNoResponse) { + _mockLink->setHashCheckNoResponse(true); + } + _connectAndWaitForParams(); + + } else { + // PX4 path where params never become ready (both timers exhaust) + _mockLink = _startPX4MockLinkNoIncrement(failMode); + if (hashCheckNoResponse) { + _mockLink->setHashCheckNoResponse(true); + } + + MultiVehicleManager *const vehicleMgr = MultiVehicleManager::instance(); + QVERIFY(vehicleMgr); + + QSignalSpy spyVehicle(vehicleMgr, &MultiVehicleManager::activeVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyVehicle, TestTimeout::mediumMs()); + + Vehicle *const vehicle = vehicleMgr->activeVehicle(); + QVERIFY(vehicle); + + QSignalSpy spyParamsReady(vehicleMgr, &MultiVehicleManager::parameterReadyVehicleAvailableChanged); + const int maxWaitMs = ParameterManager::kHashCheckTimeoutMs + + ParameterManager::kTestMaxInitialRequestTimeMs + + TestTimeout::shortMs(); + QVERIFY_NO_SIGNAL_WAIT(spyParamsReady, maxWaitMs); + } + + // Phase 4: Verify outcomes + QCOMPARE(_mockLink->hashCheckRequestCount() > 0, xHashCheck); + + if (xReady) { + Vehicle *const vehicle = MultiVehicleManager::instance()->activeVehicle(); + QVERIFY(vehicle); + QVERIFY(vehicle->parameterManager()->parametersReady()); + QCOMPARE(vehicle->parameterManager()->missingParameters(), xMissing); + } + + // PARAM_REQUEST_LIST is only checked for PX4 (ArduPilot uses FTP) + if (px4) { + QCOMPARE(_mockLink->receivedMavlinkMessageCount(MAVLINK_MSG_ID_PARAM_REQUEST_LIST) > 0, xPRL); + } + + // Phase 5: Cleanup for tests that used _connectMockLink + if (manualRefresh || !px4) { + _disconnectMockLink(); + } +} + +UT_REGISTER_TEST(HashCheckTest, TestLabel::Integration, TestLabel::Vehicle, TestLabel::Serial) diff --git a/test/FactSystem/HashCheckTest.h b/test/FactSystem/HashCheckTest.h new file mode 100644 index 000000000000..6f89f22beb98 --- /dev/null +++ b/test/FactSystem/HashCheckTest.h @@ -0,0 +1,23 @@ +#pragma once + +#include "BaseClasses/VehicleTestManualConnect.h" + +/// Data-driven test matrix for the _HASH_CHECK parameter cache optimization. +/// See _hashCheckMatrix_data() for the full scenario table. +class HashCheckTest : public VehicleTestManualConnect +{ + Q_OBJECT + +private slots: + void cleanup() override; + + void _hashCheckMatrix_data(); + void _hashCheckMatrix(); + +private: + void _deleteCacheFiles(); + void _connectAndWaitForParams(); + void _disconnectAndSettle(); + MockLink *_startPX4MockLinkNoIncrement(MockConfiguration::FailureMode_t failureMode = MockConfiguration::FailNone); + MockLink *_startPX4MockLinkHighLatency(); +}; From 5f532338476b92e84ede48fb5e9ac6ad135053b3 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 28 Feb 2026 16:46:26 -0800 Subject: [PATCH 008/119] FactSystem: bind delayed load progress reset to QObject lifetime --- src/FactSystem/ParameterManager.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FactSystem/ParameterManager.cc b/src/FactSystem/ParameterManager.cc index 6a845f298361..d0d4333616a1 100644 --- a/src/FactSystem/ParameterManager.cc +++ b/src/FactSystem/ParameterManager.cc @@ -1009,7 +1009,7 @@ void ParameterManager::_tryCacheHashLoad(int vehicleId, int componentId, const Q // Hide 500ms after animation finishes connect(ani, &QVariantAnimation::finished, this, [this] { - QTimer::singleShot(500, [this] { + QTimer::singleShot(500, this, [this] { _setLoadProgress(0); }); }); From 121c7a506d14927c3b8c8496332bd708dcd6fe49 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 1 Mar 2026 09:30:51 -0800 Subject: [PATCH 009/119] PlanMasterController dirty state cleanup and signal consistency --- src/MissionManager/PlanMasterController.cc | 123 ++++++----- src/MissionManager/PlanMasterController.h | 33 ++- src/QmlControls/PlanToolBarIndicators.qml | 22 +- src/QmlControls/PlanView.qml | 11 +- src/UI/MainWindow.qml | 4 +- .../PlanMasterControllerTest.cc | 192 ++++++++++++++++++ .../MissionManager/PlanMasterControllerTest.h | 21 ++ 7 files changed, 334 insertions(+), 72 deletions(-) diff --git a/src/MissionManager/PlanMasterController.cc b/src/MissionManager/PlanMasterController.cc index cfdbe94eb6d9..b9792f13b365 100644 --- a/src/MissionManager/PlanMasterController.cc +++ b/src/MissionManager/PlanMasterController.cc @@ -55,7 +55,6 @@ PlanMasterController::PlanMasterController(MAV_AUTOPILOT firmwareType, MAV_TYPE void PlanMasterController::_commonInit(void) { - _previousOverallDirty = dirty(); connect(&_missionController, &MissionController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty); connect(&_geoFenceController, &GeoFenceController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty); connect(&_rallyPointController, &RallyPointController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty); @@ -157,8 +156,11 @@ void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle) } else { // We are in the Plan view. if (containsItems()) { + // We have a plan which is from a different vehicle than the new active vehicle. By definition this plan requires and upload. + _setDirtyForUpload(true); + // The plan view has a stale plan in it - if (dirty()) { + if (dirtyForSave()) { // Plan is dirty, the user must decide what to do in all cases qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous dirty plan exists, no new active vehicle, sending promptForPlanUsageOnVehicleChange signal"; emit promptForPlanUsageOnVehicleChange(); @@ -176,6 +178,8 @@ void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle) } } else { // There is no previous Plan in the view + _setDirtyForSave(false); + _setDirtyForUpload(false); if (newOffline) { // Nothing special to do in this case qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - No previous plan, no longer connected to vehicle, nothing to do"; @@ -190,7 +194,8 @@ void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle) // Vehicle changed so we need to signal everything emit containsItemsChanged(); emit syncInProgressChanged(); - emit dirtyChanged(dirty()); + emit dirtyForSaveChanged(dirtyForSave()); + emit dirtyForUploadChanged(dirtyForUpload()); _updatePlanCreatorsList(); } @@ -209,16 +214,15 @@ void PlanMasterController::loadFromVehicle(void) } if (offline()) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while offline"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while offline"; } else if (_flyView) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called from Fly view"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called from Fly view"; } else if (syncInProgress()) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while syncInProgress"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while syncInProgress"; } else { _loadGeoFence = true; qCDebug(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle calling _missionController.loadFromVehicle"; _missionController.loadFromVehicle(); - setDirty(false); } } @@ -236,7 +240,6 @@ void PlanMasterController::_loadMissionComplete(void) _geoFenceController.removeAll(); _loadGeoFenceComplete(); } - setDirty(false); } } @@ -252,13 +255,14 @@ void PlanMasterController::_loadGeoFenceComplete(void) _rallyPointController.removeAll(); _loadRallyPointsComplete(); } - setDirty(false); } } void PlanMasterController::_loadRallyPointsComplete(void) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadRallyPointsComplete"; + _setDirtyForSave(true); + _setDirtyForUpload(false); } void PlanMasterController::_sendMissionComplete(void) @@ -273,7 +277,6 @@ void PlanMasterController::_sendMissionComplete(void) qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle GeoFence not supported skipping"; _sendGeoFenceComplete(); } - setDirty(false); } } @@ -294,6 +297,7 @@ void PlanMasterController::_sendGeoFenceComplete(void) void PlanMasterController::_sendRallyPointsComplete(void) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Point send complete"; + _setDirtyForUpload(false); if (_deleteWhenSendCompleted) { this->deleteLater(); } @@ -320,7 +324,6 @@ void PlanMasterController::sendToVehicle(void) qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start mission sendToVehicle"; _sendGeoFence = true; _missionController.sendToVehicle(); - setDirty(false); } } @@ -391,14 +394,12 @@ void PlanMasterController::loadFromFile(const QString& filename) if(success){ _currentPlanFile = QString::asprintf("%s/%s.%s", fileInfo.path().toLocal8Bit().data(), fileInfo.completeBaseName().toLocal8Bit().data(), AppSettings::planFileExtension); + _setDirtyForSave(false); + _setDirtyForUpload(true); } else { _currentPlanFile.clear(); } emit currentPlanFileChanged(); - - if (!offline()) { - setDirty(true); - } } QJsonDocument PlanMasterController::saveToJson() @@ -423,18 +424,21 @@ QJsonDocument PlanMasterController::saveToJson() return QJsonDocument(planJson); } -void +bool PlanMasterController::saveToCurrent() { - if(!_currentPlanFile.isEmpty()) { - saveToFile(_currentPlanFile); + if (!_currentPlanFile.isEmpty()) { + const bool saveSuccess = saveToFile(_currentPlanFile); + return saveSuccess; } + + return false; } -void PlanMasterController::saveToFile(const QString& filename) +bool PlanMasterController::saveToFile(const QString& filename) { if (filename.isEmpty()) { - return; + return false; } QString planFilename = filename; @@ -446,21 +450,22 @@ void PlanMasterController::saveToFile(const QString& filename) if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qgcApp()->showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString())); - _currentPlanFile.clear(); - emit currentPlanFileChanged(); + return false; } else { - QJsonDocument saveDoc = saveToJson(); - file.write(saveDoc.toJson()); + const QByteArray saveBytes = saveToJson().toJson(); + const qint64 bytesWritten = file.write(saveBytes); + if (bytesWritten != saveBytes.size()) { + qgcApp()->showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString())); + return false; + } if(_currentPlanFile != planFilename) { _currentPlanFile = planFilename; emit currentPlanFileChanged(); } + _setDirtyForSave(false); } - // Only clear dirty bit if we are offline - if (offline()) { - setDirty(false); - } + return true; } void PlanMasterController::saveToKml(const QString& filename) @@ -489,13 +494,18 @@ void PlanMasterController::saveToKml(const QString& filename) void PlanMasterController::removeAll(void) { + _suppressOverallDirtyUpdate = true; _missionController.removeAll(); _geoFenceController.removeAll(); _rallyPointController.removeAll(); + _missionController.setDirty(false); + _geoFenceController.setDirty(false); + _rallyPointController.setDirty(false); + _suppressOverallDirtyUpdate = false; + + _setDirtyForSave(false); + _setDirtyForUpload(false); if (_offline) { - _missionController.setDirty(false); - _geoFenceController.setDirty(false); - _rallyPointController.setDirty(false); _currentPlanFile.clear(); emit currentPlanFileChanged(); } @@ -512,7 +522,7 @@ void PlanMasterController::removeAllFromVehicle(void) if (_rallyPointController.supported()) { _rallyPointController.removeAllFromVehicle(); } - setDirty(false); + _setDirtyForUpload(false); } else { qWarning() << "PlanMasterController::removeAllFromVehicle called while offline"; } @@ -524,18 +534,6 @@ bool PlanMasterController::containsItems(void) const return _missionController.containsItems() || _geoFenceController.containsItems() || _rallyPointController.containsItems(); } -bool PlanMasterController::dirty(void) const -{ - return _missionController.dirty() || _geoFenceController.dirty() || _rallyPointController.dirty(); -} - -void PlanMasterController::setDirty(bool dirty) -{ - _missionController.setDirty(dirty); - _geoFenceController.setDirty(dirty); - _rallyPointController.setDirty(dirty); -} - QString PlanMasterController::fileExtension(void) const { return AppSettings::planFileExtension; @@ -586,6 +584,13 @@ void PlanMasterController::_showPlanFromManagerVehicle(void) _rallyPointController.showPlanFromManagerVehicle(); } } + + // Showing the vehicle plan should leave both dirty states clean. + _missionController.setDirty(false); + _geoFenceController.setDirty(false); + _rallyPointController.setDirty(false); + _setDirtyForSave(false); + _setDirtyForUpload(false); } bool PlanMasterController::syncInProgress(void) const @@ -604,9 +609,33 @@ bool PlanMasterController::isEmpty(void) const void PlanMasterController::_updateOverallDirty(void) { - if(_previousOverallDirty != dirty()){ - _previousOverallDirty = dirty(); - emit dirtyChanged(_previousOverallDirty); + if (syncInProgress() || _suppressOverallDirtyUpdate) { + return; + } + + const bool saveDirty = _missionController.dirty() || _geoFenceController.dirty() || _rallyPointController.dirty(); + if (saveDirty) { + _setDirtyForSave(true); + } +} + +void PlanMasterController::_setDirtyForSave(bool dirtyForSave) +{ + if (_dirtyForSave != dirtyForSave) { + _dirtyForSave = dirtyForSave; + emit dirtyForSaveChanged(_dirtyForSave); + + if (_dirtyForSave) { + _setDirtyForUpload(true); + } + } +} + +void PlanMasterController::_setDirtyForUpload(bool dirtyForUpload) +{ + if (_dirtyForUpload != dirtyForUpload) { + _dirtyForUpload = dirtyForUpload; + emit dirtyForUploadChanged(_dirtyForUpload); } } diff --git a/src/MissionManager/PlanMasterController.h b/src/MissionManager/PlanMasterController.h index 7f2c08dcb1a9..4c5a72ca74df 100644 --- a/src/MissionManager/PlanMasterController.h +++ b/src/MissionManager/PlanMasterController.h @@ -14,6 +14,9 @@ class QGCCompressionJob; class QmlObjectListModel; class MultiVehicleManager; class Vehicle; +#ifdef QGC_UNITTEST_BUILD +class PlanMasterControllerTest; +#endif /// Master controller for mission, fence, rally class PlanMasterController : public QObject @@ -23,6 +26,10 @@ class PlanMasterController : public QObject Q_MOC_INCLUDE("QmlObjectListModel.h") Q_MOC_INCLUDE("Vehicle.h") +#ifdef QGC_UNITTEST_BUILD + friend class PlanMasterControllerTest; +#endif + public: PlanMasterController(QObject* parent = nullptr); #ifdef QT_DEBUG @@ -41,7 +48,8 @@ class PlanMasterController : public QObject Q_PROPERTY(bool offline READ offline NOTIFY offlineChanged) ///< true: controller is not connected to an active vehicle Q_PROPERTY(bool containsItems READ containsItems NOTIFY containsItemsChanged) ///< true: Elemement is non-empty Q_PROPERTY(bool syncInProgress READ syncInProgress NOTIFY syncInProgressChanged) ///< true: Information is currently being saved/sent, false: no active save/send in progress - Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) ///< true: Unsaved/sent changes are present, false: no changes since last save/send + Q_PROPERTY(bool dirtyForSave READ dirtyForSave NOTIFY dirtyForSaveChanged) ///< true: Unsaved changes to disk are present, false: no changes since last save to disk + Q_PROPERTY(bool dirtyForUpload READ dirtyForUpload NOTIFY dirtyForUploadChanged) ///< true: Unsent changes are present, false: no changes since last upload/download sync Q_PROPERTY(QString fileExtension READ fileExtension CONSTANT) ///< File extension for missions Q_PROPERTY(QString kmlFileExtension READ kmlFileExtension CONSTANT) Q_PROPERTY(QString currentPlanFile READ currentPlanFile NOTIFY currentPlanFileChanged) @@ -79,8 +87,8 @@ class PlanMasterController : public QObject /// @param archivePath Path to the archive file Q_INVOKABLE void loadFromArchive(const QString& archivePath); - Q_INVOKABLE void saveToCurrent(); - Q_INVOKABLE void saveToFile(const QString& filename); + Q_INVOKABLE bool saveToCurrent(); + Q_INVOKABLE bool saveToFile(const QString& filename); Q_INVOKABLE void saveToKml(const QString& filename); Q_INVOKABLE void removeAll(void); ///< Removes all from controller only, synce required to remove from vehicle Q_INVOKABLE void removeAllFromVehicle(void); ///< Removes all from vehicle and controller @@ -92,8 +100,8 @@ class PlanMasterController : public QObject bool offline (void) const { return _offline; } bool containsItems (void) const; bool syncInProgress (void) const; - bool dirty (void) const; - void setDirty (bool dirty); + bool dirtyForSave (void) const { return _dirtyForSave; } + bool dirtyForUpload (void) const { return _dirtyForUpload; } QString fileExtension (void) const; QString kmlFileExtension(void) const; QString currentPlanFile (void) const { return _currentPlanFile; } @@ -119,7 +127,8 @@ class PlanMasterController : public QObject signals: void containsItemsChanged (); void syncInProgressChanged (void); - void dirtyChanged (bool dirty); + void dirtyForSaveChanged (bool dirtyForSave); + void dirtyForUploadChanged (bool dirtyForUpload); void offlineChanged (bool offlineEditing); void currentPlanFileChanged (void); void planCreatorsChanged (QmlObjectListModel* planCreators); @@ -142,6 +151,14 @@ private slots: private: void _commonInit (void); void _showPlanFromManagerVehicle(void); + void _setDirtyForSave(bool dirtyForSave); + void _setDirtyForUpload(bool dirtyForUpload); + +#ifdef QGC_UNITTEST_BUILD + // Used by unit tests to set dirty flags for initial state + void _setDirtyForSaveUnitTest(bool dirtyForSave) { _dirtyForSave = dirtyForSave; } + void _setDirtyForUploadUnitTest(bool dirtyForUpload) { _dirtyForUpload = dirtyForUpload; } +#endif MultiVehicleManager* _multiVehicleMgr = nullptr; Vehicle* _controllerVehicle = nullptr; ///< Offline controller vehicle @@ -157,7 +174,9 @@ private slots: bool _sendRallyPoints = false; QString _currentPlanFile; bool _deleteWhenSendCompleted = false; - bool _previousOverallDirty = false; + bool _dirtyForSave = false; + bool _dirtyForUpload = false; + bool _suppressOverallDirtyUpdate = false; QmlObjectListModel* _planCreators = nullptr; bool _manualCreation = false; QGCCompressionJob* _extractionJob = nullptr; diff --git a/src/QmlControls/PlanToolBarIndicators.qml b/src/QmlControls/PlanToolBarIndicators.qml index 8002ac204aff..997be633428f 100644 --- a/src/QmlControls/PlanToolBarIndicators.qml +++ b/src/QmlControls/PlanToolBarIndicators.qml @@ -19,7 +19,8 @@ RowLayout { property var _geoFenceController: _planMasterController.geoFenceController property var _rallyPointController: _planMasterController.rallyPointController property bool _controllerOffline: _planMasterController.offline - property var _controllerDirty: _planMasterController.dirty + property var _saveDirty: _planMasterController.dirtyForSave + property var _uploadDirty: _planMasterController.dirtyForUpload property var _syncInProgress: _planMasterController.syncInProgress property var _visualItems: _missionController.visualItems property bool _hasPlanItems: _planMasterController.containsItems @@ -31,9 +32,9 @@ RowLayout { } function _downloadClicked() { - if (_planMasterController.dirty) { + if (_saveDirty) { QGroundControl.showMessageDialog(root, qsTr("Download"), - qsTr("You have unsaved/unsent changes. Downloading from the Vehicle will lose these changes. Are you sure?"), + qsTr("You have unsaved changes. Downloading from the Vehicle will lose these changes. Are you sure?"), Dialog.Yes | Dialog.Cancel, function() { _planMasterController.loadFromVehicle() }) } else { @@ -42,7 +43,7 @@ RowLayout { } function _openButtonClicked() { - if (_planMasterController.dirty) { + if (_saveDirty || _uploadDirty) { QGroundControl.showMessageDialog(root, qsTr("Open Plan"), qsTr("You have unsaved/unsent changes. Loading a new Plan will lose these changes. Are you sure?"), Dialog.Yes | Dialog.Cancel, @@ -54,10 +55,11 @@ RowLayout { function _saveButtonClicked() { if(_planMasterController.currentPlanFile !== "") { - _planMasterController.saveToCurrent() - QGroundControl.showMessageDialog(root, qsTr("Save"), - qsTr("Plan saved to `%1`").arg(_planMasterController.currentPlanFile), - Dialog.Ok) + if (_planMasterController.saveToCurrent()) { + QGroundControl.showMessageDialog(root, qsTr("Save"), + qsTr("Plan saved to `%1`").arg(_planMasterController.currentPlanFile), + Dialog.Ok) + } } else { _planMasterController.saveToSelectedFile() } @@ -107,7 +109,7 @@ RowLayout { text: _planMasterController.currentPlanFile === "" ? qsTr("Save As") : qsTr("Save") iconSource: "/res/SaveToDisk.svg" enabled: !_syncInProgress && _hasPlanItems - primary: _controllerDirty + primary: _saveDirty onClicked: _saveButtonClicked() } @@ -117,7 +119,7 @@ RowLayout { iconSource: "/res/UploadToVehicle.svg" enabled: !_syncInProgress && _hasPlanItems visible: !_syncInProgress - primary: _controllerDirty + primary: _uploadDirty onClicked: _uploadClicked() } diff --git a/src/QmlControls/PlanView.qml b/src/QmlControls/PlanView.qml index 2234a19b830b..53572059c35a 100644 --- a/src/QmlControls/PlanView.qml +++ b/src/QmlControls/PlanView.qml @@ -207,11 +207,13 @@ Item { onAcceptedForSave: (file) => { if (planFiles) { - _planMasterController.saveToFile(file) + if (_planMasterController.saveToFile(file)) { + close() + } } else { _planMasterController.saveToKml(file) + close() } - close() } onAcceptedForLoad: (file) => { @@ -675,7 +677,7 @@ Item { QGCButton { Layout.fillWidth: true - text: _planMasterController.dirty ? + text: (_planMasterController.dirtyForSave) ? (_planMasterController.managerVehicle.isOfflineEditingVehicle ? qsTr("Discard Unsaved Changes") : qsTr("Discard Unsaved Changes, Load New Plan From Vehicle")) : qsTr("Load New Plan From Vehicle") onClicked: { @@ -690,9 +692,6 @@ Item { text: _planMasterController.managerVehicle.isOfflineEditingVehicle ? qsTr("Keep Current Plan") : qsTr("Keep Current Plan, Don't Update From Vehicle") onClicked: { - if (!_planMasterController.managerVehicle.isOfflineEditingVehicle) { - _planMasterController.dirty = true - } _promptForPlanUsageShowing = false close() } diff --git a/src/UI/MainWindow.qml b/src/UI/MainWindow.qml index 8d43c719df16..4278b780ed9a 100644 --- a/src/UI/MainWindow.qml +++ b/src/UI/MainWindow.qml @@ -213,9 +213,9 @@ ApplicationWindow { property string closeDialogTitle: qsTr("Close %1").arg(QGroundControl.appName) function checkForUnsavedMission() { - if (planView._planMasterController.dirty) { + if (planView._planMasterController.dirtyForSave || planView._planMasterController.dirtyForUpload) { QGroundControl.showMessageDialog(mainWindow, closeDialogTitle, - qsTr("You have a mission edit in progress which has not been saved/sent. If you close you will lose changes. Are you sure you want to close?"), + qsTr("You have a mission edit in progress which has not been saved/uploaded. If you close you will lose changes. Are you sure you want to close?"), Dialog.Yes | Dialog.No, function() { _closeChecksToSkip |= _skipUnsavedMissionCheckMask; performCloseChecks() }) return false diff --git a/test/MissionManager/PlanMasterControllerTest.cc b/test/MissionManager/PlanMasterControllerTest.cc index 5580e418b2fb..0e97fd993d80 100644 --- a/test/MissionManager/PlanMasterControllerTest.cc +++ b/test/MissionManager/PlanMasterControllerTest.cc @@ -6,6 +6,11 @@ #include "PlanMasterController.h" #include "Vehicle.h" +#include +#include +#include +#include + void PlanMasterControllerTest::init() { UnitTest::init(); @@ -47,15 +52,202 @@ void PlanMasterControllerTest::_testActiveVehicleChanged() QVERIFY(spyMissionManager.onlyEmittedOnceByMask(missionManagerErrorSignalMask)); spyMissionManager.clearSignal("error"); QVERIFY(spyMissionManager.noneEmitted()); + _connectMockLink(MAV_AUTOPILOT_PX4); auto masterControllerMgrVehicleChanged = spyMasterController.mask("managerVehicleChanged"); QVERIFY(spyMasterController.emittedOnceByMask(masterControllerMgrVehicleChanged)); + emit outgoingManagerVehicle->missionManager()->error(0, ""); // This signal was affected by the defect - it wouldn't reach the subscriber. Here // we make sure it does. QVERIFY(spyMissionManager.onlyEmittedOnceByMask(missionManagerErrorSignalMask)); } +void PlanMasterControllerTest::_testDirtyFlagsMatrix_data() +{ + // Dirty-state transition matrix ("unchanged" means preserve prior value): + // + // | State \ Action | Upload OK | Clear | SaveDirty=true | Load plan | Save file OK | Clear save-dirty | + // |----------------|-----------|-------|----------------|-----------|--------------|------------------| + // | dirtyForSave | unchanged | false | true | false | false | false | + // | dirtyForUpload | false | false | true | true | unchanged | unchanged | + + // Data columns: + // - scenario: DirtyScenario enum value selecting which action path to execute + // - initialDirtyForSave: initial dirtyForSave state before action (DirtyStateTrue/False) + // - initialDirtyForUpload: initial dirtyForUpload state before action (DirtyStateTrue/False) + // - expectedDirtyForSave: expected final dirtyForSave state (DirtyState) + // - expectedDirtyForUpload: expected final dirtyForUpload state (DirtyState) + + QTest::addColumn("scenario"); + QTest::addColumn("initialDirtyForSave"); + QTest::addColumn("initialDirtyForUpload"); + QTest::addColumn("expectedDirtyForSave"); + QTest::addColumn("expectedDirtyForUpload"); + + struct ScenarioExpectation { + DirtyScenario scenario; + const char* name; + DirtyState expectedDirtyForSave; + DirtyState expectedDirtyForUpload; + }; + + const QList scenarioExpectations = { + { UploadPreservesSaveDirtyFalse, "upload completion keeps save false", DirtyStateUnchanged, DirtyStateFalse }, + { UploadPreservesSaveDirtyTrue, "upload completion keeps save true", DirtyStateUnchanged, DirtyStateFalse }, + { UploadFalseOnPlanClear, "upload false on clear", DirtyStateFalse, DirtyStateFalse }, + { UploadTrueWhenSaveTrue, "upload true when save true", DirtyStateTrue, DirtyStateTrue }, + { UploadTrueOnNewPlanLoad, "upload true on new plan load", DirtyStateFalse, DirtyStateTrue }, + { SaveToFilePreservesUploadDirtyTrue, "saveToFile keeps upload true", DirtyStateFalse, DirtyStateUnchanged }, + { SaveToFilePreservesUploadDirtyFalse,"saveToFile keeps upload false", DirtyStateFalse, DirtyStateUnchanged }, + { SaveFalseOnSuccessfulLoad, "save false on successful load", DirtyStateFalse, DirtyStateTrue }, + { ClearSaveDirtyPreservesUploadTrue, "clear save dirty keeps upload true", DirtyStateFalse, DirtyStateUnchanged }, + { ClearSaveDirtyPreservesUploadFalse, "clear save dirty keeps upload false", DirtyStateFalse, DirtyStateUnchanged }, + }; + + const QList initialStates = { + DirtyStateFalse, + DirtyStateTrue, + }; + + for (const ScenarioExpectation& expectation : scenarioExpectations) { + for (const DirtyState initialDirtyForSave : initialStates) { + for (const DirtyState initialDirtyForUpload : initialStates) { + DirtyState expectedDirtyForSave = expectation.expectedDirtyForSave; + DirtyState expectedDirtyForUpload = expectation.expectedDirtyForUpload; + + if ((expectation.scenario == UploadTrueWhenSaveTrue) && (initialDirtyForSave == DirtyStateTrue)) { + // _setDirtyForSave(true) only drives dirtyForUpload when dirtyForSave transitions false->true. + // If dirtyForSave already starts true, dirtyForUpload is preserved. + expectedDirtyForUpload = DirtyStateUnchanged; + } + + const QString rowName = QStringLiteral("%1 [init save=%2 upload=%3]") + .arg(expectation.name) + .arg(initialDirtyForSave == DirtyStateTrue ? QStringLiteral("true") : QStringLiteral("false")) + .arg(initialDirtyForUpload == DirtyStateTrue ? QStringLiteral("true") : QStringLiteral("false")); + QTest::newRow(rowName.toLatin1().constData()) + << +expectation.scenario + << +initialDirtyForSave + << +initialDirtyForUpload + << +expectedDirtyForSave + << +expectedDirtyForUpload; + } + } + } +} + +void PlanMasterControllerTest::_testDirtyFlagsMatrix() +{ + QFETCH(int, scenario); + QFETCH(int, initialDirtyForSave); + QFETCH(int, initialDirtyForUpload); + QFETCH(int, expectedDirtyForSave); + QFETCH(int, expectedDirtyForUpload); + + QVERIFY(initialDirtyForSave != DirtyStateUnchanged); + QVERIFY(initialDirtyForUpload != DirtyStateUnchanged); + + const auto dirtyStateToBool = [](int state) -> bool { + switch (state) { + case DirtyStateFalse: + return false; + case DirtyStateTrue: + return true; + default: + Q_ASSERT(false); // Invalid test data + return false; + } + }; + + _masterController->_setDirtyForSaveUnitTest(dirtyStateToBool(initialDirtyForSave)); + _masterController->_setDirtyForUploadUnitTest(dirtyStateToBool(initialDirtyForUpload)); + + const bool initialDirtyForSaveBool = _masterController->dirtyForSave(); + const bool initialDirtyForUploadBool = _masterController->dirtyForUpload(); + + QSignalSpy dirtyForSaveChangedSpy(_masterController, &PlanMasterController::dirtyForSaveChanged); + QSignalSpy dirtyForUploadChangedSpy(_masterController, &PlanMasterController::dirtyForUploadChanged); + + switch (scenario) { + case UploadPreservesSaveDirtyFalse: { + const bool invoked = QMetaObject::invokeMethod(_masterController, "_sendRallyPointsComplete", Qt::DirectConnection); + QVERIFY(invoked); + break; + } + case UploadPreservesSaveDirtyTrue: { + const bool invoked = QMetaObject::invokeMethod(_masterController, "_sendRallyPointsComplete", Qt::DirectConnection); + QVERIFY(invoked); + break; + } + case UploadFalseOnPlanClear: + _masterController->removeAll(); + break; + case UploadTrueWhenSaveTrue: + _masterController->_setDirtyForSave(true); + break; + case UploadTrueOnNewPlanLoad: + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + break; + case SaveToFilePreservesUploadDirtyTrue: { + const QString saveFile = QDir::temp().filePath(QStringLiteral("qgc_planmaster_test_%1.plan").arg(QDateTime::currentMSecsSinceEpoch())); + QVERIFY(_masterController->saveToFile(saveFile)); + QFile::remove(saveFile); + break; + } + case SaveToFilePreservesUploadDirtyFalse: { + const QString saveFile = QDir::temp().filePath(QStringLiteral("qgc_planmaster_test_%1.plan").arg(QDateTime::currentMSecsSinceEpoch())); + QVERIFY(_masterController->saveToFile(saveFile)); + QFile::remove(saveFile); + break; + } + case SaveFalseOnSuccessfulLoad: + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + break; + case ClearSaveDirtyPreservesUploadTrue: + _masterController->_setDirtyForSave(false); + break; + case ClearSaveDirtyPreservesUploadFalse: + _masterController->_setDirtyForSave(false); + break; + } + + const auto resolveExpected = [](int expectedState, bool unchangedValue) -> bool { + switch (expectedState) { + case DirtyStateFalse: + return false; + case DirtyStateTrue: + return true; + case DirtyStateUnchanged: + return unchangedValue; + } + return unchangedValue; + }; + + const bool expectedDirtyForSaveBool = resolveExpected(expectedDirtyForSave, initialDirtyForSaveBool); + const bool expectedDirtyForUploadBool = resolveExpected(expectedDirtyForUpload, initialDirtyForUploadBool); + const int expectedDirtyForSaveSignalCount = (expectedDirtyForSaveBool != initialDirtyForSaveBool) ? 1 : 0; + const int expectedDirtyForUploadSignalCount = (expectedDirtyForUploadBool != initialDirtyForUploadBool) ? 1 : 0; + + QCOMPARE(_masterController->dirtyForSave(), expectedDirtyForSaveBool); + QCOMPARE(_masterController->dirtyForUpload(), expectedDirtyForUploadBool); + + QCOMPARE(dirtyForSaveChangedSpy.count(), expectedDirtyForSaveSignalCount); + QCOMPARE(dirtyForUploadChangedSpy.count(), expectedDirtyForUploadSignalCount); + + if (dirtyForSaveChangedSpy.count() > 0) { + const QList& args = dirtyForSaveChangedSpy.at(dirtyForSaveChangedSpy.count() - 1); + QCOMPARE(args.count(), 1); + QCOMPARE(args.first().toBool(), _masterController->dirtyForSave()); + } + + if (dirtyForUploadChangedSpy.count() > 0) { + const QList& args = dirtyForUploadChangedSpy.at(dirtyForUploadChangedSpy.count() - 1); + QCOMPARE(args.count(), 1); + QCOMPARE(args.first().toBool(), _masterController->dirtyForUpload()); + } +} + #include "UnitTest.h" UT_REGISTER_TEST(PlanMasterControllerTest, TestLabel::Integration, TestLabel::MissionManager) diff --git a/test/MissionManager/PlanMasterControllerTest.h b/test/MissionManager/PlanMasterControllerTest.h index 3f2a3da3a193..cc1f5fcefbf7 100644 --- a/test/MissionManager/PlanMasterControllerTest.h +++ b/test/MissionManager/PlanMasterControllerTest.h @@ -14,7 +14,28 @@ private slots: void _testMissionPlannerFileLoad(); void _testActiveVehicleChanged(); + void _testDirtyFlagsMatrix_data(); + void _testDirtyFlagsMatrix(); private: + enum DirtyScenario { + UploadPreservesSaveDirtyTrue, + UploadPreservesSaveDirtyFalse, + UploadFalseOnPlanClear, + UploadTrueWhenSaveTrue, + UploadTrueOnNewPlanLoad, + SaveToFilePreservesUploadDirtyTrue, + SaveToFilePreservesUploadDirtyFalse, + SaveFalseOnSuccessfulLoad, + ClearSaveDirtyPreservesUploadTrue, + ClearSaveDirtyPreservesUploadFalse, + }; + + enum DirtyState { + DirtyStateFalse, + DirtyStateTrue, + DirtyStateUnchanged + }; + PlanMasterController* _masterController = nullptr; }; From cc29733480bde9669c6e65ddb185f9c99cf58209 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 1 Mar 2026 10:10:20 -0800 Subject: [PATCH 010/119] Promote guard-condition logs to qCCritical across plan controllers Use qCCritical for offline/syncInProgress/flyView guard conditions in loadFromVehicle, sendToVehicle, removeAllFromVehicle, and showPlanFromManagerVehicle. Critical-level logs cause unit tests to fail, ensuring these invalid call sequences are caught during testing. Also converts bare qWarning() in PlanMasterController::removeAllFromVehicle to categorized qCCritical for consistency. Affected controllers: - PlanMasterController - MissionController - GeoFenceController - RallyPointController --- src/MissionManager/GeoFenceController.cc | 14 +++++++------- src/MissionManager/MissionController.cc | 14 +++++++------- src/MissionManager/PlanMasterController.cc | 6 +++--- src/MissionManager/RallyPointController.cc | 14 +++++++------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/MissionManager/GeoFenceController.cc b/src/MissionManager/GeoFenceController.cc index 799aedbdb82b..8e60144206a8 100644 --- a/src/MissionManager/GeoFenceController.cc +++ b/src/MissionManager/GeoFenceController.cc @@ -215,9 +215,9 @@ void GeoFenceController::removeAll(void) void GeoFenceController::removeAllFromVehicle(void) { if (_masterController->offline()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::removeAllFromVehicle called while offline"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::removeAllFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::removeAllFromVehicle called while syncInProgress"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::removeAllFromVehicle called while syncInProgress"; } else { _geoFenceManager->removeAll(); } @@ -226,9 +226,9 @@ void GeoFenceController::removeAllFromVehicle(void) void GeoFenceController::loadFromVehicle(void) { if (_masterController->offline()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::loadFromVehicle called while offline"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::loadFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::loadFromVehicle called while syncInProgress"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::loadFromVehicle called while syncInProgress"; } else { _itemsRequested = true; _geoFenceManager->loadFromVehicle(); @@ -238,9 +238,9 @@ void GeoFenceController::loadFromVehicle(void) void GeoFenceController::sendToVehicle(void) { if (_masterController->offline()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle called while offline"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle called while syncInProgress"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle called while syncInProgress"; } else { qCDebug(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle"; _geoFenceManager->sendToVehicle(_breachReturnPoint, _polygons, _circles); @@ -357,7 +357,7 @@ bool GeoFenceController::showPlanFromManagerVehicle(void) { qCDebug(GeoFenceControllerLog) << "showPlanFromManagerVehicle _flyView" << _flyView; if (_masterController->offline()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::showPlanFromManagerVehicle called while offline"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::showPlanFromManagerVehicle called while offline"; return true; // stops further propagation of showPlanFromManagerVehicle due to error } else { _itemsRequested = true; diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 9af6fcffff83..79f39b271d8f 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -203,9 +203,9 @@ void MissionController::_newMissionItemsAvailableFromVehicle(bool removeAllReque void MissionController::loadFromVehicle(void) { if (_masterController->offline()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while offline"; + qCCritical(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while syncInProgress"; + qCCritical(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while syncInProgress"; } else { _itemsRequested = true; _managerVehicle->missionManager()->loadFromVehicle(); @@ -215,9 +215,9 @@ void MissionController::loadFromVehicle(void) void MissionController::sendToVehicle(void) { if (_masterController->offline()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while offline"; + qCCritical(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while syncInProgress"; + qCCritical(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while syncInProgress"; } else { qCDebug(MissionControllerLog) << "MissionControllerLog::sendToVehicle"; if (_visualItems->count() == 1) { @@ -2203,9 +2203,9 @@ bool MissionController::containsItems(void) const void MissionController::removeAllFromVehicle(void) { if (_masterController->offline()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while offline"; + qCCritical(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while syncInProgress"; + qCCritical(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while syncInProgress"; } else { _itemsRequested = true; _missionManager->removeAll(); @@ -2272,7 +2272,7 @@ bool MissionController::showPlanFromManagerVehicle (void) { qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle _flyView" << _flyView; if (_masterController->offline()) { - qCWarning(MissionControllerLog) << "MissionController::showPlanFromManagerVehicle called while offline"; + qCCritical(MissionControllerLog) << "MissionController::showPlanFromManagerVehicle called while offline"; return true; // stops further propagation of showPlanFromManagerVehicle due to error } else { if (!_managerVehicle->initialPlanRequestComplete()) { diff --git a/src/MissionManager/PlanMasterController.cc b/src/MissionManager/PlanMasterController.cc index b9792f13b365..5b3ed3c31c85 100644 --- a/src/MissionManager/PlanMasterController.cc +++ b/src/MissionManager/PlanMasterController.cc @@ -317,9 +317,9 @@ void PlanMasterController::sendToVehicle(void) } if (offline()) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while offline"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while syncInProgress"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while syncInProgress"; } else { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start mission sendToVehicle"; _sendGeoFence = true; @@ -524,7 +524,7 @@ void PlanMasterController::removeAllFromVehicle(void) } _setDirtyForUpload(false); } else { - qWarning() << "PlanMasterController::removeAllFromVehicle called while offline"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::removeAllFromVehicle called while offline"; } setManualCreation(false); } diff --git a/src/MissionManager/RallyPointController.cc b/src/MissionManager/RallyPointController.cc index c37b031660c7..46c46745b066 100644 --- a/src/MissionManager/RallyPointController.cc +++ b/src/MissionManager/RallyPointController.cc @@ -133,9 +133,9 @@ void RallyPointController::removeAll(void) void RallyPointController::removeAllFromVehicle(void) { if (_masterController->offline()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::removeAllFromVehicle called while offline"; + qCCritical(RallyPointControllerLog) << "RallyPointController::removeAllFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::removeAllFromVehicle called while syncInProgress"; + qCCritical(RallyPointControllerLog) << "RallyPointController::removeAllFromVehicle called while syncInProgress"; } else { _rallyPointManager->removeAll(); } @@ -144,9 +144,9 @@ void RallyPointController::removeAllFromVehicle(void) void RallyPointController::loadFromVehicle(void) { if (_masterController->offline()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::loadFromVehicle called while offline"; + qCCritical(RallyPointControllerLog) << "RallyPointController::loadFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::loadFromVehicle called while syncInProgress"; + qCCritical(RallyPointControllerLog) << "RallyPointController::loadFromVehicle called while syncInProgress"; } else { _itemsRequested = true; _rallyPointManager->loadFromVehicle(); @@ -156,9 +156,9 @@ void RallyPointController::loadFromVehicle(void) void RallyPointController::sendToVehicle(void) { if (_masterController->offline()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::sendToVehicle called while offline"; + qCCritical(RallyPointControllerLog) << "RallyPointController::sendToVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::sendToVehicle called while syncInProgress"; + qCCritical(RallyPointControllerLog) << "RallyPointController::sendToVehicle called while syncInProgress"; } else { qCDebug(RallyPointControllerLog) << "RallyPointController::sendToVehicle"; setDirty(false); @@ -290,7 +290,7 @@ bool RallyPointController::showPlanFromManagerVehicle (void) { qCDebug(RallyPointControllerLog) << "showPlanFromManagerVehicle _flyView" << _flyView; if (_masterController->offline()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::showPlanFromManagerVehicle called while offline"; + qCCritical(RallyPointControllerLog) << "RallyPointController::showPlanFromManagerVehicle called while offline"; return true; // stops further propagation of showPlanFromManagerVehicle due to error } else { if (!_managerVehicle->initialPlanRequestComplete()) { From 0cfc7998152b465bcff4b89cefb6d37f8caf0f00 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 1 Mar 2026 10:36:37 -0800 Subject: [PATCH 011/119] Fix transient dirtyForUpload toggling in _loadRallyPointsComplete Add _setDirtyStates() helper that atomically updates both dirtyForSave and dirtyForUpload flags, emitting at most one signal per flag only when the value actually changes. This eliminates the cascading issue where _setDirtyForSave(true) would trigger _setDirtyForUpload(true), followed immediately by _setDirtyForUpload(false), producing a transient incorrect state and redundant signal emissions during load-from-vehicle completion. Replace all paired _setDirtyForSave/_setDirtyForUpload call sites with the new atomic helper. --- src/MissionManager/PlanMasterController.cc | 31 +++++++++++++++------- src/MissionManager/PlanMasterController.h | 1 + 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/MissionManager/PlanMasterController.cc b/src/MissionManager/PlanMasterController.cc index 5b3ed3c31c85..3947578b059a 100644 --- a/src/MissionManager/PlanMasterController.cc +++ b/src/MissionManager/PlanMasterController.cc @@ -178,8 +178,7 @@ void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle) } } else { // There is no previous Plan in the view - _setDirtyForSave(false); - _setDirtyForUpload(false); + _setDirtyStates(false, false); if (newOffline) { // Nothing special to do in this case qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - No previous plan, no longer connected to vehicle, nothing to do"; @@ -261,8 +260,7 @@ void PlanMasterController::_loadGeoFenceComplete(void) void PlanMasterController::_loadRallyPointsComplete(void) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadRallyPointsComplete"; - _setDirtyForSave(true); - _setDirtyForUpload(false); + _setDirtyStates(true /* dirtyForSave */, false /* dirtyForUpload */); } void PlanMasterController::_sendMissionComplete(void) @@ -394,8 +392,7 @@ void PlanMasterController::loadFromFile(const QString& filename) if(success){ _currentPlanFile = QString::asprintf("%s/%s.%s", fileInfo.path().toLocal8Bit().data(), fileInfo.completeBaseName().toLocal8Bit().data(), AppSettings::planFileExtension); - _setDirtyForSave(false); - _setDirtyForUpload(true); + _setDirtyStates(false /* dirtyForSave */, true /* dirtyForUpload */); } else { _currentPlanFile.clear(); } @@ -503,8 +500,7 @@ void PlanMasterController::removeAll(void) _rallyPointController.setDirty(false); _suppressOverallDirtyUpdate = false; - _setDirtyForSave(false); - _setDirtyForUpload(false); + _setDirtyStates(false, false); if (_offline) { _currentPlanFile.clear(); emit currentPlanFileChanged(); @@ -589,8 +585,7 @@ void PlanMasterController::_showPlanFromManagerVehicle(void) _missionController.setDirty(false); _geoFenceController.setDirty(false); _rallyPointController.setDirty(false); - _setDirtyForSave(false); - _setDirtyForUpload(false); + _setDirtyStates(false, false); } bool PlanMasterController::syncInProgress(void) const @@ -639,6 +634,22 @@ void PlanMasterController::_setDirtyForUpload(bool dirtyForUpload) } } +void PlanMasterController::_setDirtyStates(bool dirtyForSave, bool dirtyForUpload) +{ + const bool saveChanged = (_dirtyForSave != dirtyForSave); + const bool uploadChanged = (_dirtyForUpload != dirtyForUpload); + + _dirtyForSave = dirtyForSave; + _dirtyForUpload = dirtyForUpload; + + if (saveChanged) { + emit dirtyForSaveChanged(_dirtyForSave); + } + if (uploadChanged) { + emit dirtyForUploadChanged(_dirtyForUpload); + } +} + void PlanMasterController::_updatePlanCreatorsList(void) { if (!_flyView) { diff --git a/src/MissionManager/PlanMasterController.h b/src/MissionManager/PlanMasterController.h index 4c5a72ca74df..5f0457297a03 100644 --- a/src/MissionManager/PlanMasterController.h +++ b/src/MissionManager/PlanMasterController.h @@ -153,6 +153,7 @@ private slots: void _showPlanFromManagerVehicle(void); void _setDirtyForSave(bool dirtyForSave); void _setDirtyForUpload(bool dirtyForUpload); + void _setDirtyStates(bool dirtyForSave, bool dirtyForUpload); #ifdef QGC_UNITTEST_BUILD // Used by unit tests to set dirty flags for initial state From 0ccf0d8f85c041a8d70c491ab85aeb1c001d0913 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 1 Mar 2026 14:15:44 -0800 Subject: [PATCH 012/119] PlanView: Replace direct map-click and right-click with toolstrip toggle buttons for Waypoint and ROI - Add checkable Waypoint toggle button (stays toggled on; each map click adds a waypoint) - Add checkable ROI toggle button (toggles off after single click; shows insert/cancel dialog when ROI is active) - Remove right-click/press-and-hold ROI insertion (_mapRightClicked) - Make Waypoint and ROI toggles mutually exclusive - Remove toolbar help text for waypoint/ROI click instructions (PlanToolBarIndicators) - Clean up dead code: remove 18 unused property declarations (button indices, stale properties, unused QGCMapPalette) - Fix dynamically created ROI DropPanel leak (onClosed: destroy()) - Add standard globals.parent explanatory comment --- src/QmlControls/PlanToolBarIndicators.qml | 25 ------ src/QmlControls/PlanView.qml | 98 +++++++++++------------ 2 files changed, 45 insertions(+), 78 deletions(-) diff --git a/src/QmlControls/PlanToolBarIndicators.qml b/src/QmlControls/PlanToolBarIndicators.qml index 997be633428f..a72511c16297 100644 --- a/src/QmlControls/PlanToolBarIndicators.qml +++ b/src/QmlControls/PlanToolBarIndicators.qml @@ -142,31 +142,6 @@ RowLayout { } } - ColumnLayout { - Layout.alignment: Qt.AlignVCenter - spacing: 0 - - QGCLabel { - text: _leftClickText() - font.pointSize: ScreenTools.smallFontPointSize - visible: _editingLayer === _layerMission || _editingLayer === _layerRally - - function _leftClickText() { - if (_editingLayer === _layerMission) { - return qsTr("- Click on the map to add Waypoint") - } else { - return qsTr("- Click on the map to add Rally Point") - } - } - } - - QGCLabel { - text: qsTr("- %1 to add ROI %2").arg(ScreenTools.isMobile ? qsTr("Press and hold") : qsTr("Right click")).arg(_missionController.isROIActive ? qsTr("or Cancel ROI") : "") - font.pointSize: ScreenTools.smallFontPointSize - visible: _editingLayer === _layerMission && _planMasterController.controllerVehicle.roiModeSupported - } - } - Component { id: hamburgerDropPanelComponent diff --git a/src/QmlControls/PlanView.qml b/src/QmlControls/PlanView.qml index 53572059c35a..3b9abd247c8b 100644 --- a/src/QmlControls/PlanView.qml +++ b/src/QmlControls/PlanView.qml @@ -18,30 +18,25 @@ Item { readonly property int _decimalPlaces: 8 readonly property real _margin: ScreenTools.defaultFontPixelHeight * 0.5 readonly property real _toolsMargin: ScreenTools.defaultFontPixelWidth * 0.75 - readonly property real _radius: ScreenTools.defaultFontPixelWidth * 0.5 readonly property real _rightPanelWidth: Math.min(width / 3, ScreenTools.defaultFontPixelWidth * 30) - readonly property var _defaultVehicleCoordinate: QtPositioning.coordinate(37.803784, -122.462276) - readonly property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly property var _planMasterController: planMasterController property var _missionController: _planMasterController.missionController property var _geoFenceController: _planMasterController.geoFenceController property var _rallyPointController: _planMasterController.rallyPointController property var _visualItems: _missionController.visualItems - property bool _lightWidgetBorders: editorMap.isSatelliteMap property bool _singleComplexItem: _missionController.complexMissionItemNames.length === 1 property int _editingLayer: _layerMission - property int _toolStripBottom: toolStrip.height + toolStrip.y property var _appSettings: QGroundControl.settingsManager.appSettings property var _planViewSettings: QGroundControl.settingsManager.planViewSettings property bool _promptForPlanUsageShowing: false + property bool _addROIOnClick: false + property bool _addWaypointOnClick: false readonly property int _layerMission: 1 readonly property int _layerFence: 2 readonly property int _layerRally: 3 - readonly property string _armedVehicleUploadPrompt: qsTr("Vehicle is currently armed. Do you want to upload the mission to the vehicle?") - onVisibleChanged: { if(visible) { editorMap.zoomLevel = QGroundControl.flightMapZoom @@ -57,11 +52,6 @@ Item { return coordinate } - property bool _firstMissionLoadComplete: false - property bool _firstFenceLoadComplete: false - property bool _firstRallyLoadComplete: false - property bool _firstLoadComplete: false - MapFitFunctions { id: mapFitFunctions // The name for this id cannot be changed without breaking references outside of this code. Beware! map: editorMap @@ -257,8 +247,6 @@ Item { // Initial map position duplicates Fly view position Component.onCompleted: editorMap.center = QGroundControl.flightMapPosition - QGCMapPalette { id: mapPal; lightColors: editorMap.isSatelliteMap } - onZoomLevelChanged: { QGroundControl.flightMapZoom = editorMap.zoomLevel } @@ -279,7 +267,20 @@ Item { switch (_editingLayer) { case _layerMission: - insertSimpleItemAfterCurrent(coordinate) + if (_addROIOnClick) { + _addROIOnClick = false + if (_missionController.isROIActive) { + var pos = Qt.point(mouse.x, mouse.y) + // For some strange reason using mainWindow in mapToItem doesn't work, so we use globals.parent instead which also gets us mainWindow + pos = editorMap.mapToItem(globals.parent, pos) + var dropPanel = insertOrCancelROIDropPanelComponent.createObject(mainWindow, { mapClickCoord: coordinate, clickRect: Qt.rect(pos.x, pos.y, 0, 0) }) + dropPanel.open() + } else { + insertROIAfterCurrent(coordinate) + } + } else if (_addWaypointOnClick) { + insertSimpleItemAfterCurrent(coordinate) + } break case _layerRally: if (_rallyPointController.supported) { @@ -289,35 +290,6 @@ Item { } } - function _mapRightClicked(position) { - // Take focus to close any previous editing - editorMap.focus = true - if (!mainWindow.allowViewSwitch()) { - return - } - var coordinate = editorMap.toCoordinate(position, false /* clipToViewPort */) - coordinate.latitude = coordinate.latitude.toFixed(_decimalPlaces) - coordinate.longitude = coordinate.longitude.toFixed(_decimalPlaces) - coordinate.altitude = coordinate.altitude.toFixed(_decimalPlaces) - - if (_editingLayer === _layerMission) { - if (!planMasterController.controllerVehicle.roiModeSupported) { - return - } - if (_missionController.isROIActive) { - // For some strange reason using mainWindow in mapToItem doesn't work, so we use globals.parent instead which also gets us mainWindow - position = editorMap.mapToItem(globals.parent, position) - var dropPanel = insertOrCancelROIDropPanelComponent.createObject(mainWindow, { mapClickCoord: coordinate, clickRect: Qt.rect(position.x, position.y, 0, 0) }) - dropPanel.open() - } else { - insertROIAfterCurrent(coordinate) - } - } - } - - onMapRightClicked: (position) => _mapRightClicked(position) - onMapPressAndHold: (position) => _mapRightClicked(position) - // Add the mission item visuals to the map Repeater { model: _missionController.visualItems @@ -427,17 +399,20 @@ Item { maxHeight: parent.height - toolStrip.y visible: _editingLayer == _layerMission - readonly property int fileButtonIndex: 0 - readonly property int takeoffButtonIndex: 1 - readonly property int waypointButtonIndex: 2 - readonly property int roiButtonIndex: 3 - readonly property int patternButtonIndex: 4 - readonly property int landButtonIndex: 5 - readonly property int centerButtonIndex: 6 - - property bool _isRallyLayer: _editingLayer == _layerRally property bool _isMissionLayer: _editingLayer == _layerMission + Binding { + target: waypointButton + property: "checked" + value: _addWaypointOnClick + } + + Binding { + target: roiButton + property: "checked" + value: _addROIOnClick + } + ToolStripActionList { id: toolStripActionList model: [ @@ -462,6 +437,22 @@ Item { } } }, + ToolStripAction { + id: waypointButton + text: qsTr("Waypoint") + iconSource: "/res/waypoint.svg" + visible: toolStrip._isMissionLayer + checkable: true + onTriggered: { _addWaypointOnClick = !_addWaypointOnClick; if (_addWaypointOnClick) _addROIOnClick = false } + }, + ToolStripAction { + id: roiButton + text: qsTr("ROI") + iconSource: "/qmlimages/roi.svg" + visible: toolStrip._isMissionLayer && _planMasterController.controllerVehicle.roiModeSupported + checkable: true + onTriggered: { _addROIOnClick = !_addROIOnClick; if (_addROIOnClick) _addWaypointOnClick = false } + }, ToolStripAction { text: _planMasterController.controllerVehicle.multiRotor ? qsTr("Return") @@ -705,6 +696,7 @@ Item { DropPanel { id: insertOrCancelROIDropPanel + onClosed: destroy() property var mapClickCoord From f1fd993940b3052f550fca3e1071ba8629785390 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 1 Mar 2026 15:19:24 -0800 Subject: [PATCH 013/119] docs: Update fly_view_toolbar.md with missing indicator sections and screenshots - Add missing indicator sections: GPS Resilience, ESC, Joystick, RTK GPS, Telemetry RSSI, RC RSSI, Gimbal, VTOL Transitions, Multi-Vehicle Selector, APM Support Forwarding - Promote 'Other Indicators' items to top-level sections with descriptions - Merge GPS and RTK GPS into single 'GPS / RTK GPS' section - Add inline heading screenshots for all indicators that have assets - Add new screenshot assets for all indicators - Fix typos: acess, availble, give you, a multiple --- .../fly/toolbar/apm_support_indicator.png | Bin 0 -> 11822 bytes docs/assets/fly/toolbar/battery_indicator.png | Bin 7353 -> 16158 bytes docs/assets/fly/toolbar/esc_indicator.png | Bin 0 -> 14263 bytes .../fly/toolbar/flight_modes_indicator.png | Bin 7023 -> 16541 bytes docs/assets/fly/toolbar/gimbal_indicator.png | Bin 0 -> 22037 bytes docs/assets/fly/toolbar/gps_indicator.png | Bin 7749 -> 16630 bytes .../assets/fly/toolbar/joystick_indicator.png | Bin 0 -> 10906 bytes .../fly/toolbar/main_status_indicator.png | Bin 0 -> 17396 bytes .../fly/toolbar/multi_vehicle_indicator.png | Bin 0 -> 23522 bytes docs/assets/fly/toolbar/rc_rssi_indicator.png | Bin 0 -> 10537 bytes .../fly/toolbar/remote_id_indicator.png | Bin 0 -> 8261 bytes .../fly/toolbar/telemetry_rssi_indicator.png | Bin 0 -> 10567 bytes docs/assets/fly/toolbar/vtol_indicator.png | Bin 0 -> 17974 bytes .../fly_view/fly_view_toolbar.md | 66 +++++++++++++----- 14 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 docs/assets/fly/toolbar/apm_support_indicator.png create mode 100644 docs/assets/fly/toolbar/esc_indicator.png create mode 100644 docs/assets/fly/toolbar/gimbal_indicator.png create mode 100644 docs/assets/fly/toolbar/joystick_indicator.png create mode 100644 docs/assets/fly/toolbar/main_status_indicator.png create mode 100644 docs/assets/fly/toolbar/multi_vehicle_indicator.png create mode 100644 docs/assets/fly/toolbar/rc_rssi_indicator.png create mode 100644 docs/assets/fly/toolbar/remote_id_indicator.png create mode 100644 docs/assets/fly/toolbar/telemetry_rssi_indicator.png create mode 100644 docs/assets/fly/toolbar/vtol_indicator.png diff --git a/docs/assets/fly/toolbar/apm_support_indicator.png b/docs/assets/fly/toolbar/apm_support_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..d0f63475e9be8a29f759c39d4dbeed813cf8a06d GIT binary patch literal 11822 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 a;9Y{Uk#k%`hU5PY-2g>dRbaKWY4CqoqFI9g literal 0 HcmV?d00001 diff --git a/docs/assets/fly/toolbar/battery_indicator.png b/docs/assets/fly/toolbar/battery_indicator.png index c26b0fce9e26ab147ff9300b49f7099eb50a0369..6f5b84ed3f27075351e80e11a1c42878734a2e3d 100644 GIT binary patch literal 16158 zcmV+(KjFZMP)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^000SaNLh0L02U7C0T~C4dnC03ZNKL_t(|oV}fSykZO!h;wD zq?5~j1V^hmnY?8%plKS0O@oLqb}h7yv1@TI#BnLD4UOh3;W}%jw#Q!!LKDgm@z+h$ z&`3km2--N2JPUx45Ih^@~R>k67D!!nz%kPAuob#9y0|84MbxhEoiuhD;5od>=Ft=-zZQ z28NhIZSk=(0jT=B3s#MkgEo=Ybz*0Y3+W{m=K+PU*E#d?za0Cf+0t}T7 zwqd@*JYnrU{442JoCZcUIQmJfl(YB#8fC*9Jz%JRpimEKWs}WW( zEHevAI2|#G8@LXk)R?Bmra~a0o84* zh|ti`N?U+6@#qefL?TV2cvlo?>`?ct;|fl@9F?poO0NstP_Kg4uF^MIMauMr=#Q~W z9-$y@9j!P-JeRc+)qX<&Y?Z?n36yyG01TZGI1oRcF>xhRsXZbhfpnEwP& z#(|*p2)bxDcnQ$B^gG4R8>P&^Ift}{5jf}RYY3vMfoXMzlY*md!AY)ev~7bZP+ReR zEY5$d4PcniG67p~pps;vXGIjywlnI%jJw!y8OWUftpgL?r6OGo?TJ4VlunC~) zJ8{(*vyk?QtL8whkUn9#L!@pzan9?qI2tE;d8V!kRLi~9h^Um53HBszU9|99=PF>Y zfrzC2NXic+4RRzA)O4g6(AI0|+u@w+5HkCzgL;{2K^y9ugH^JA@$^fKLcyO)ve5iE zn37TI4XjXq9JHPpv{?^2ph3if*3mRA&NW>XGy*m@Jm;Lw{H=xbT=g(7O1r!>4<#`B zg69;TaE`>GTo9*GB@k#_M2%1DV<18_1f&Xfs!1>kwK$kOanK zA^Lt)hc?oEn6E~z0k(?Rx;`~& zyc@NxH~iSP!-gw4bPSU|U9iYsb=S_5&{-0Eotj;op&@Zi zs|GZI)3#AK8$HT&U>s$JK5@b%fu+b;0hOgzYA`CMc=*$TdU?p@%dQXF5 z#f)fb6e60rz&=e(sE(V)3XkTl?=&8sFnAOZgO|p zdaAb}o2?^pzJ$HHa#8+z{7?<)jS2ukcdcnCCbUJVX-3TFG%&VB;?_we5DUf*u!1Kb z&Jd>las_u)x(y?zi9oHxHQki}+J22Hb-EaWgpnK)B}LwYIoDxJ*_b${{Wg#C=z*x} zQ6Nz2vt$Cn}K6F z6@z|pOygSberq9h!hVFnsbZa#J=f@92Ye5moZ>W8z2vg3DGxBx$q#

iU*>sA?A3 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 literal 7353 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 diff --git a/docs/assets/fly/toolbar/esc_indicator.png b/docs/assets/fly/toolbar/esc_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..1576b67bf142f8f74560f27cd4911fe0e91749b2 GIT binary patch literal 14263 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 literal 0 HcmV?d00001 diff --git a/docs/assets/fly/toolbar/flight_modes_indicator.png b/docs/assets/fly/toolbar/flight_modes_indicator.png index e3481618ca871c868c5a4abd7351fd952238eafb..821a72535077652084eb456edce4256bf03cce82 100644 GIT binary patch literal 16541 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

GJmnPWZXsSGQN6#84d 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

l7+L8 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 rr`}m)Pf+5Zv7f?he7-g1bC2GylEw-*;c{ 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 literal 0 HcmV?d00001 diff --git a/docs/assets/fly/toolbar/main_status_indicator.png b/docs/assets/fly/toolbar/main_status_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..61b677ce34c530d791e7893c5feb6a1e5298c1e6 GIT binary patch literal 17396 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 literal 0 HcmV?d00001 diff --git a/docs/assets/fly/toolbar/multi_vehicle_indicator.png b/docs/assets/fly/toolbar/multi_vehicle_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..71b5db858628a8688c369efaa06bad2cd3fca0a3 GIT binary patch literal 23522 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 literal 0 HcmV?d00001 diff --git a/docs/assets/fly/toolbar/remote_id_indicator.png b/docs/assets/fly/toolbar/remote_id_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..cf65d4c645a5424e642d722cd068e669450af14b GIT binary patch literal 8261 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

Dt@U$pV`Yj#8RT;v?x#BVFz!` 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 D=A}4^ literal 0 HcmV?d00001 diff --git a/docs/assets/fly/toolbar/telemetry_rssi_indicator.png b/docs/assets/fly/toolbar/telemetry_rssi_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..5a23ada7d5cd9185b39c722a7fa7f2710bf317a3 GIT binary patch literal 10567 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 literal 0 HcmV?d00001 diff --git a/docs/assets/fly/toolbar/vtol_indicator.png b/docs/assets/fly/toolbar/vtol_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..d6f8221952fcb4553b78850f6c0ba1b5ed4e5d3c GIT binary patch literal 17974 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 literal 0 HcmV?d00001 diff --git a/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md b/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md index 50457b484cbe..0adbc5390dba 100644 --- a/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md +++ b/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md @@ -13,11 +13,11 @@ The "Q" icon on the left of the toolbar allows you to select between additional ## Toolbar Indicators -Next are a multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. +Next are multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. ![Toolbar Indicator - expand button](../../../assets/fly/toolbar_indicator_expand.png) -### Flight Status +### Flight Status Flight Status indicator The Flight Status indicator shows you whether the vehicle is ready to fly or not. It can be in one of the following states: @@ -29,10 +29,10 @@ The Flight Status indicator shows you whether the vehicle is ready to fly or not - **Landing** - Vehicle is in the process of landing. - **Communication Lost** - QGroundControl has lost communication with the vehicle. -The Flight Status Indicator dropdown also gives you acess to: +The Flight Status indicator dropdown also gives you access to: - **Arm** - Arming a vehicle starts the motors in preparation for takeoff. You will only be able to arm the vehicle if it is safe and ready to fly. Generally you do not need to manually arm the vehicle. You can simply takeoff or start a mission and the vehicle will arm itself. -- **Disarm** - Disarming a vehicle is only availble when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. +- **Disarm** - Disarming a vehicle is only available when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. - **Emergency Stop** - Emergency stop is used to disarm the vehicle while it is flying. For emergency use only, your vehicle will crash! In the cases of warnings or not ready state you can click the indicator to display the dropdown which will show the reason(s) why. The toggle on the right expands each error with additional information and possible solutions. @@ -41,7 +41,7 @@ In the cases of warnings or not ready state you can click the indicator to displ Once each issue is resolved it will disappear from the UI. When all issues blocking arming have been removed you should now be ready to fly. -## Flight Mode +### Flight Mode Flight Mode indicator The Flight Mode indicator shows you the current flight mode. The dropdown allows you to switch between flight modes. The expanded page allows you to: @@ -49,15 +49,23 @@ The Flight Mode indicator shows you the current flight mode. The dropdown allows - Set global geo-fence settings - Add/Remove flight modes from the displayed list -## Vehicle Messages +### Vehicle Messages Vehicle Messages indicator The Vehicle Messages indicator dropdown shows you messages which come from the vehicle. The indicator will turn red if there are important messages available. -## GPS +### GPS / RTK GPS GPS / RTK GPS indicator -The GPS indicator shows you the satellite count and the HDOP in the toolbar icon. The dropdown shows you additional GPS status. The expanded page give you access to RTK settings. +The GPS/RTK GPS indicator shows satellite and GNSS status in the toolbar, and the dropdown provides additional GPS details. -## Battery +With an active vehicle, the indicator shows vehicle GPS information (for example, satellite count and HDOP), and the expanded page provides access to RTK-related settings. + +When there is no active vehicle but RTK is connected, the indicator switches to RTK status so you can still monitor the correction link. + +### GPS Resilience + +The GPS Resilience indicator appears when the vehicle reports GPS resilience telemetry (authentication, spoofing, or jamming state). The dropdown provides summary status and per-GPS details when available. + +### Battery Battery indicator The Battery indicator shows you a configurable colored battery icon for remaining charge. It can also be configured to show percent remaining, voltage or both. The expanded page allows you to: @@ -65,14 +73,38 @@ The Battery indicator shows you a configurable colored battery icon for remainin - Configure the icon coloring - Set up the low battery failsafe -## Remote ID +### Remote ID Remote ID indicator + +The Remote ID indicator appears when Remote ID is available on the active vehicle. Its color indicates overall Remote ID health, and the dropdown shows Remote ID status details. + +### ESC ESC indicator + +The ESC indicator appears when ESC telemetry is available from the vehicle. It shows overall ESC health and online motor count, and opens a detailed ESC status page. + +### Joystick Joystick indicator + +The Joystick indicator appears when a joystick/gamepad is detected. The dropdown shows device status and connection details. + +### Telemetry RSSI Telemetry RSSI indicator + +The Telemetry RSSI indicator appears when telemetry signal information is available. It provides local/remote RSSI and additional radio link quality details. + +### RC RSSI RC RSSI indicator + +The RC RSSI indicator appears when RC signal information is available. It shows current RC link strength and opens a page with RC RSSI details. + +### Gimbal Gimbal indicator + +The Gimbal indicator is shown when the vehicle supports the [MAVLink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html). It displays active gimbal status and provides access to gimbal controls and settings. + +### VTOL Transitions VTOL indicator + +For VTOL vehicles, a VTOL transition status indicator is shown when applicable. It indicates the current VTOL mode/state and provides transition-related status information. + +### Multi-Vehicle Selector Multi-Vehicle indicator -## Other Indicators +The Multi-Vehicle selector appears when more than one vehicle is connected. It allows you to quickly switch the active vehicle from the toolbar. -There are other indicators which only show in certain situations: +### APM Support Forwarding APM Support Forwarding indicator -* Telemetry RSSI -* RC RSSI -* Gimbal - Only displayed if the vehicle supports the [Mavlink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html) -* VTOL transitions -* Select from multiple connected vehicles +On ArduPilot, an APM Support Forwarding indicator appears when MAVLink traffic forwarding to a support server is enabled. From fb529db72f6416b53370f3a6d62ac194b5e93db2 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 1 Mar 2026 19:35:36 -0800 Subject: [PATCH 014/119] Plan toolbar: add currentPlanFileName property and improve toolbar UX --- src/MissionManager/PlanMasterController.h | 7 +++++-- src/QmlControls/PlanToolBarIndicators.qml | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/MissionManager/PlanMasterController.h b/src/MissionManager/PlanMasterController.h index 5f0457297a03..db9560976cca 100644 --- a/src/MissionManager/PlanMasterController.h +++ b/src/MissionManager/PlanMasterController.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -52,7 +53,8 @@ class PlanMasterController : public QObject Q_PROPERTY(bool dirtyForUpload READ dirtyForUpload NOTIFY dirtyForUploadChanged) ///< true: Unsent changes are present, false: no changes since last upload/download sync Q_PROPERTY(QString fileExtension READ fileExtension CONSTANT) ///< File extension for missions Q_PROPERTY(QString kmlFileExtension READ kmlFileExtension CONSTANT) - Q_PROPERTY(QString currentPlanFile READ currentPlanFile NOTIFY currentPlanFileChanged) + Q_PROPERTY(QString currentPlanFile READ currentPlanFile NOTIFY currentPlanFileChanged) ///< Fully qualified path, empty if not yet saved + Q_PROPERTY(QString currentPlanFileName READ currentPlanFileName NOTIFY currentPlanFileChanged) ///< File name only, no path or extension Q_PROPERTY(QStringList loadNameFilters READ loadNameFilters CONSTANT) ///< File filter list loading plan files Q_PROPERTY(QStringList saveNameFilters READ saveNameFilters CONSTANT) ///< File filter list saving plan files Q_PROPERTY(QmlObjectListModel* planCreators MEMBER _planCreators NOTIFY planCreatorsChanged) @@ -104,7 +106,8 @@ class PlanMasterController : public QObject bool dirtyForUpload (void) const { return _dirtyForUpload; } QString fileExtension (void) const; QString kmlFileExtension(void) const; - QString currentPlanFile (void) const { return _currentPlanFile; } + QString currentPlanFile (void) const { return _currentPlanFile; } + QString currentPlanFileName (void) const { return QFileInfo(_currentPlanFile).completeBaseName(); } QStringList loadNameFilters (void) const; QStringList saveNameFilters (void) const; bool isEmpty (void) const; diff --git a/src/QmlControls/PlanToolBarIndicators.qml b/src/QmlControls/PlanToolBarIndicators.qml index a72511c16297..79567cbe87f3 100644 --- a/src/QmlControls/PlanToolBarIndicators.qml +++ b/src/QmlControls/PlanToolBarIndicators.qml @@ -106,7 +106,7 @@ RowLayout { } QGCButton { - text: _planMasterController.currentPlanFile === "" ? qsTr("Save As") : qsTr("Save") + text: _planMasterController.currentPlanFile === "" ? qsTr("Save As") : _planMasterController.currentPlanFileName iconSource: "/res/SaveToDisk.svg" enabled: !_syncInProgress && _hasPlanItems primary: _saveDirty @@ -166,7 +166,7 @@ RowLayout { QGCButton { Layout.fillWidth: true text: qsTr("Download") - enabled: !_syncInProgress + enabled: !_syncInProgress && !_controllerOffline visible: !_syncInProgress onClicked: { From 5df175564613bd06ab25736ec2ef35b0ffa5e011 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 4 Mar 2026 11:38:43 -0800 Subject: [PATCH 015/119] ComponentInformation: improve metadata loading diagnostics - Add structured summary log per metadata type showing source (cache/FTP/HTTP), primary vs fallback, translation status, and URI - Consolidate key diagnostic messages under ComponentInformationManagerLog category - Add componentName parameter to translation warnings for context - Skip translation download for English locales (metadata is authored in English) - Log cache location on startup - Log primary/fallback request attempts and download failures - Demote intermediate CRC/cache messages to RequestMetaDataTypeStateMachineLog - Replace pointer-identity metadata source tracking with explicit flag - Track actual URI used (primary or fallback) for accurate failure reporting - Remove unit-test-only restriction on FTP/HTTP failure logging --- .../ComponentInformation/CompInfoGeneral.cc | 3 +- .../ComponentInformationManager.cc | 1 + .../ComponentInformationTranslation.cc | 24 +++-- .../ComponentInformationTranslation.h | 4 +- .../RequestMetaDataTypeStateMachine.cc | 91 +++++++++++++++---- .../RequestMetaDataTypeStateMachine.h | 13 ++- .../ComponentInformationTranslationTest.cc | 14 ++- 7 files changed, 117 insertions(+), 33 deletions(-) diff --git a/src/Vehicle/ComponentInformation/CompInfoGeneral.cc b/src/Vehicle/ComponentInformation/CompInfoGeneral.cc index f362e68be851..18483e8907b6 100644 --- a/src/Vehicle/ComponentInformation/CompInfoGeneral.cc +++ b/src/Vehicle/ComponentInformation/CompInfoGeneral.cc @@ -1,4 +1,5 @@ #include "CompInfoGeneral.h" +#include "ComponentInformationManager.h" #include "JsonHelper.h" #include "JsonParsing.h" #include "QGCLoggingCategory.h" @@ -72,7 +73,7 @@ void CompInfoGeneral::setJson(const QString& metadataJsonFileName) if (uris.uriMetaData.isEmpty() || !uris.crcMetaDataValid) { // The CRC is optional for dynamically updated metadata, and once we want to support that this logic needs // to be updated. - qCDebug(CompInfoGeneralLog) << "Metadata missing fields: type:uri:crcValid" << metadataType << + qCDebug(ComponentInformationManagerLog) << "Metadata missing fields: type:uri:crcValid" << metadataType << uris.uriMetaData << uris.crcMetaDataValid; continue; } diff --git a/src/Vehicle/ComponentInformation/ComponentInformationManager.cc b/src/Vehicle/ComponentInformation/ComponentInformationManager.cc index a141ac81616d..8546ffdeeb9c 100644 --- a/src/Vehicle/ComponentInformation/ComponentInformationManager.cc +++ b/src/Vehicle/ComponentInformation/ComponentInformationManager.cc @@ -21,6 +21,7 @@ ComponentInformationManager::ComponentInformationManager(Vehicle *vehicle, QObje , _translation(new ComponentInformationTranslation(this, _cachedFileDownload)) { qCDebug(ComponentInformationManagerLog) << this; + qCDebug(ComponentInformationManagerLog) << "Cache location:" << QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/QGCCompInfoCache"); _compInfoMap[MAV_COMP_ID_AUTOPILOT1][COMP_METADATA_TYPE_GENERAL] = new CompInfoGeneral (MAV_COMP_ID_AUTOPILOT1, vehicle, this); _compInfoMap[MAV_COMP_ID_AUTOPILOT1][COMP_METADATA_TYPE_PARAMETER] = new CompInfoParam (MAV_COMP_ID_AUTOPILOT1, vehicle, this); diff --git a/src/Vehicle/ComponentInformation/ComponentInformationTranslation.cc b/src/Vehicle/ComponentInformation/ComponentInformationTranslation.cc index f7160ca1c5c3..bd2d14d5266a 100644 --- a/src/Vehicle/ComponentInformation/ComponentInformationTranslation.cc +++ b/src/Vehicle/ComponentInformation/ComponentInformationTranslation.cc @@ -19,12 +19,18 @@ ComponentInformationTranslation::ComponentInformationTranslation(QObject* parent } bool ComponentInformationTranslation::downloadAndTranslate(const QString& summaryJsonFile, - const QString& toTranslateJsonFile, int maxCacheAgeSec) + const QString& toTranslateJsonFile, int maxCacheAgeSec, const QString& componentName) { + // Metadata is authored in English, no translation needed + const QString locale = QLocale::system().name(); + if (locale.startsWith(QLatin1String("en"))) { + qCDebug(ComponentInformationTranslationLog) << "Skipping translation for English locale" << locale << "for" << componentName; + return false; + } + // Parse summary: find url for current locale _toTranslateJsonFile = toTranslateJsonFile; - QString locale = QLocale::system().name(); - QString url = getUrlFromSummaryJson(summaryJsonFile, locale); + QString url = getUrlFromSummaryJson(summaryJsonFile, locale, componentName); if (url.isEmpty()) { return false; } @@ -39,26 +45,26 @@ bool ComponentInformationTranslation::downloadAndTranslate(const QString& summar return true; } -QString ComponentInformationTranslation::getUrlFromSummaryJson(const QString &summaryJsonFile, const QString &locale) +QString ComponentInformationTranslation::getUrlFromSummaryJson(const QString &summaryJsonFile, const QString &locale, const QString &componentName) { QString errorString; QJsonDocument jsonDoc; if (!JsonParsing::isJsonFile(summaryJsonFile, jsonDoc, errorString)) { - qCWarning(ComponentInformationTranslationLog) << "Metadata translation summary json file open failed:" << errorString; + qCWarning(ComponentInformationTranslationLog) << "Metadata translation summary json file open failed for" << componentName << ":" << errorString; return ""; } QJsonObject jsonObj = jsonDoc.object(); QJsonObject localeObj = jsonObj[locale].toObject(); if (localeObj.isEmpty()) { - qCWarning(ComponentInformationTranslationLog) << "Locale " << locale << " not found in json"; + qCWarning(ComponentInformationTranslationLog) << "Locale" << locale << "not found in translation json for" << componentName; return ""; } QString url = localeObj["url"].toString(); if (url.isEmpty()) { - qCWarning(ComponentInformationTranslationLog) << "Locale " << locale << ": no url set"; + qCWarning(ComponentInformationTranslationLog) << "Locale" << locale << "has no url in translation json for" << componentName; } return url; } @@ -98,7 +104,7 @@ void ComponentInformationTranslation::onDownloadCompleted(bool success, const QS QString ComponentInformationTranslation::translateJsonUsingTS(const QString &toTranslateJsonFile, const QString &tsFile) { - qCInfo(ComponentInformationTranslationLog) << "Translating" << toTranslateJsonFile << "using" << tsFile; + qCDebug(ComponentInformationTranslationLog) << "Translating" << toTranslateJsonFile << "using" << tsFile; // Open JSON and get the 'translation' object QString errorString; @@ -200,7 +206,7 @@ QString ComponentInformationTranslation::translateJsonUsingTS(const QString &toT translatedFile.write(jsonDoc.toJson()); translatedFile.close(); - qCInfo(ComponentInformationTranslationLog) << "JSON file" << toTranslateJsonFile << "successfully translated to" << translatedFileName; + qCDebug(ComponentInformationTranslationLog) << "JSON file" << toTranslateJsonFile << "successfully translated to" << translatedFileName; return translatedFileName; } diff --git a/src/Vehicle/ComponentInformation/ComponentInformationTranslation.h b/src/Vehicle/ComponentInformation/ComponentInformationTranslation.h index 94c853df28ef..480ec25a407f 100644 --- a/src/Vehicle/ComponentInformation/ComponentInformationTranslation.h +++ b/src/Vehicle/ComponentInformation/ComponentInformationTranslation.h @@ -26,7 +26,7 @@ class ComponentInformationTranslation : public QObject /// @param toTranslateJsonFile json file to be translated /// @param maxCacheAgeSec Maximum age of cached item in seconds /// @return true: Asynchronous download has started, false: Download initialization failed - bool downloadAndTranslate(const QString& summaryJsonFile, const QString& toTranslateJsonFile, int maxCacheAgeSec); + bool downloadAndTranslate(const QString& summaryJsonFile, const QString& toTranslateJsonFile, int maxCacheAgeSec, const QString& componentName); QString translateJsonUsingTS(const QString& toTranslateJsonFile, const QString& tsFile); @@ -36,7 +36,7 @@ class ComponentInformationTranslation : public QObject private slots: void onDownloadCompleted(bool success, const QString &localFile, QString errorMsg, bool fromCache); private: - QString getUrlFromSummaryJson(const QString& summaryJsonFile, const QString& locale); + QString getUrlFromSummaryJson(const QString& summaryJsonFile, const QString& locale, const QString& componentName); static QJsonObject translate(const QJsonObject& translationObj, const QHash& translations, QJsonObject doc); diff --git a/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.cc b/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.cc index c388dc90c039..04e7ad3b8597 100644 --- a/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.cc +++ b/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.cc @@ -168,14 +168,17 @@ void RequestMetaDataTypeStateMachine::_wireTimeoutHandling() void RequestMetaDataTypeStateMachine::request(CompInfo* compInfo) { + _compInfo = compInfo; qCDebug(RequestMetaDataTypeStateMachineLog) << Q_FUNC_INFO << typeToString(); - _compInfo = compInfo; _jsonMetadataFileName.clear(); _jsonMetadataTranslatedFileName.clear(); _jsonTranslationFileName.clear(); _activeAsyncState = nullptr; _activeSkippableState = nullptr; + _metadataSource = MetadataSource::None; + _metadataUri.clear(); + _metadataIsFallback = false; start(); } @@ -335,14 +338,17 @@ void RequestMetaDataTypeStateMachine::_requestMetaDataJson() const QString uri = compInfo->uriMetaData(); _jsonMetadataCrcValid = compInfo->crcMetaDataValid(); + qCDebug(ComponentInformationManagerLog) << typeToString() << ": requesting metadata (primary) from" << uri; + _activeAsyncState = _stateRequestMetaDataJson; _activeSkippableState = nullptr; - _requestFile(fileTag, compInfo->crcMetaDataValid(), uri, _jsonMetadataFileName); + _requestFile(fileTag, compInfo->crcMetaDataValid(), uri, _jsonMetadataFileName, true); } void RequestMetaDataTypeStateMachine::_requestMetaDataJsonFallback() { - qCDebug(RequestMetaDataTypeStateMachineLog) << "Trying fallback download for" << typeToString(); + qCDebug(ComponentInformationManagerLog) << typeToString() << ": primary failed, requesting metadata (fallback) from" << _compInfo->uriMetaDataFallback(); + _metadataIsFallback = true; CompInfo* compInfo = _compInfo; const QString fileTag = ComponentInformationManager::_getFileCacheTag(compInfo->type, compInfo->crcMetaDataFallback(), false); @@ -351,7 +357,7 @@ void RequestMetaDataTypeStateMachine::_requestMetaDataJsonFallback() _activeAsyncState = nullptr; _activeSkippableState = _stateRequestMetaDataJsonFallback; - _requestFile(fileTag, compInfo->crcMetaDataFallbackValid(), uri, _jsonMetadataFileName); + _requestFile(fileTag, compInfo->crcMetaDataFallbackValid(), uri, _jsonMetadataFileName, true); } void RequestMetaDataTypeStateMachine::_requestTranslationJson() @@ -359,9 +365,15 @@ void RequestMetaDataTypeStateMachine::_requestTranslationJson() CompInfo* compInfo = _compInfo; const QString uri = compInfo->uriTranslation(); + if (uri.isEmpty()) { + qCDebug(RequestMetaDataTypeStateMachineLog) << "No translation URI for" << typeToString(); + _stateRequestTranslationJson->complete(); + return; + } + _activeAsyncState = _stateRequestTranslationJson; _activeSkippableState = nullptr; - _requestFile("", false, uri, _jsonTranslationFileName); + _requestFile("", false, uri, _jsonTranslationFileName, false); } void RequestMetaDataTypeStateMachine::_requestTranslate() @@ -371,7 +383,8 @@ void RequestMetaDataTypeStateMachine::_requestTranslate() if (!_compMgr->translation()->downloadAndTranslate(_jsonTranslationFileName, _jsonMetadataFileName, - ComponentInformationManager::cachedFileMaxAgeSec)) { + ComponentInformationManager::cachedFileMaxAgeSec, + typeToString())) { disconnect(_compMgr->translation(), &ComponentInformationTranslation::downloadComplete, this, &RequestMetaDataTypeStateMachine::_downloadAndTranslationComplete); qCDebug(RequestMetaDataTypeStateMachineLog) << "downloadAndTranslate() failed"; @@ -394,11 +407,14 @@ void RequestMetaDataTypeStateMachine::_downloadAndTranslationComplete(QString tr void RequestMetaDataTypeStateMachine::_completeRequest() { - if (_jsonMetadataTranslatedFileName.isEmpty()) { - _compInfo->setJson(_jsonMetadataFileName); - } else { + const bool success = !_jsonMetadataFileName.isEmpty(); + const bool translated = !_jsonMetadataTranslatedFileName.isEmpty(); + + if (translated) { _compInfo->setJson(_jsonMetadataTranslatedFileName); QFile(_jsonMetadataTranslatedFileName).remove(); + } else { + _compInfo->setJson(_jsonMetadataFileName); } // If we don't have a CRC we didn't cache the file and need to delete it @@ -408,9 +424,35 @@ void RequestMetaDataTypeStateMachine::_completeRequest() if (!_jsonMetadataCrcValid && !_jsonTranslationFileName.isEmpty()) { QFile(_jsonTranslationFileName).remove(); } + + // Summary log for easy filtering + const char* sourceLabel = _metadataIsFallback ? "(fallback)" : "(primary)"; + if (success) { + if (translated) { + qCDebug(ComponentInformationManagerLog) << typeToString() << ":" << _metadataSourceToString(_metadataSource) + << sourceLabel << "(translated)" << _metadataUri; + } else { + qCDebug(ComponentInformationManagerLog) << typeToString() << ":" << _metadataSourceToString(_metadataSource) + << sourceLabel << _metadataUri; + } + } else { + qCWarning(ComponentInformationManagerLog) << typeToString() << ": failed to load metadata (primary and fallback)" + << (_metadataUri.isEmpty() ? _compInfo->uriMetaData() : _metadataUri); + } } -void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, bool crcValid, const QString& uri, QString& outputFileName) +const char* RequestMetaDataTypeStateMachine::_metadataSourceToString(MetadataSource source) +{ + switch (source) { + case MetadataSource::Cache: return "loaded from cache"; + case MetadataSource::FTP: return "downloaded via FTP"; + case MetadataSource::HTTP: return "downloaded via HTTP"; + case MetadataSource::None: return "not available"; + } + return "unknown"; +} + +void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, bool crcValid, const QString& uri, QString& outputFileName, bool trackMetadataSource) { FTPManager* ftpManager = _compInfo->vehicle->ftpManager(); _currentCacheFileTag = cacheFileTag; @@ -427,7 +469,7 @@ void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, }; if (!_compInfo->available() || uri.isEmpty()) { - qCDebug(RequestMetaDataTypeStateMachineLog) << "Skipping download. Component information not available for" << _currentCacheFileTag; + qCDebug(ComponentInformationManagerLog) << typeToString() << ": metadata not available, skipping download"; completeCurrentState(); return; } @@ -436,14 +478,28 @@ void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, if (!cachedFile.isEmpty()) { qCDebug(RequestMetaDataTypeStateMachineLog) << "Using cached file" << cachedFile; + if (trackMetadataSource) { + _metadataSource = MetadataSource::Cache; + _metadataUri = uri; + } outputFileName = cachedFile; completeCurrentState(); return; } + if (!crcValid) { + qCDebug(RequestMetaDataTypeStateMachineLog) << typeToString() << ": CRC not available, cache bypassed"; + } else { + qCDebug(RequestMetaDataTypeStateMachineLog) << typeToString() << ": not found in cache, downloading"; + } + qCDebug(RequestMetaDataTypeStateMachineLog) << "Downloading json" << uri; if (_uriIsMAVLinkFTP(uri)) { + if (trackMetadataSource) { + _metadataSource = MetadataSource::FTP; + _metadataUri = uri; + } connect(ftpManager, &FTPManager::downloadComplete, this, &RequestMetaDataTypeStateMachine::_ftpDownloadComplete); if (ftpManager->download(MAV_COMP_ID_AUTOPILOT1, uri, QStandardPaths::writableLocation(QStandardPaths::TempLocation))) { _downloadStartTime.start(); @@ -454,6 +510,10 @@ void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, completeCurrentState(); } } else { + if (trackMetadataSource) { + _metadataSource = MetadataSource::HTTP; + _metadataUri = uri; + } connect(_compMgr->_cachedFileDownload, &QGCCachedFileDownload::finished, this, &RequestMetaDataTypeStateMachine::_httpDownloadComplete); if (_compMgr->_cachedFileDownload->download(uri, crcValid ? 0 : ComponentInformationManager::cachedFileMaxAgeSec)) { @@ -493,8 +553,8 @@ void RequestMetaDataTypeStateMachine::_ftpDownloadComplete(const QString& fileNa if (_currentFileName) { *_currentFileName = _downloadCompleteJsonWorker(fileName); } - } else if (qgcApp()->runningUnitTests()) { - qCWarning(RequestMetaDataTypeStateMachineLog) << "_ftpDownloadComplete failed filename:errorMsg" << fileName << errorMsg; + } else { + qCDebug(ComponentInformationManagerLog) << typeToString() << ": FTP download failed:" << errorMsg; } if (_activeAsyncState) { @@ -529,9 +589,8 @@ void RequestMetaDataTypeStateMachine::_httpDownloadComplete(bool success, const if (_currentFileName) { *_currentFileName = _downloadCompleteJsonWorker(localFile); } - } else if (qgcApp()->runningUnitTests()) { - qCWarning(RequestMetaDataTypeStateMachineLog) << "_httpDownloadComplete failed localFile:errorMsg:fromCache" - << localFile << errorMsg << fromCache; + } else { + qCDebug(ComponentInformationManagerLog) << typeToString() << ": HTTP download failed:" << errorMsg; } if (_activeAsyncState) { diff --git a/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.h b/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.h index 2aed8736e63f..34099faede4e 100644 --- a/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.h +++ b/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.h @@ -53,10 +53,18 @@ class RequestMetaDataTypeStateMachine : public QGCStateMachine bool _shouldSkipTranslation() const; // Download helpers - void _requestFile(const QString& cacheFileTag, bool crcValid, const QString& uri, QString& outputFileName); + void _requestFile(const QString& cacheFileTag, bool crcValid, const QString& uri, QString& outputFileName, bool trackMetadataSource); QString _downloadCompleteJsonWorker(const QString& jsonFileName); static bool _uriIsMAVLinkFTP(const QString& uri); + enum class MetadataSource { + None, + Cache, + FTP, + HTTP, + }; + static const char* _metadataSourceToString(MetadataSource source); + // Message result handlers void _handleCompMetadataResult(MAV_RESULT result, const mavlink_message_t& message); void _handleCompInfoResult(MAV_RESULT result, Vehicle::RequestMessageResultHandlerFailureCode_t failureCode, const mavlink_message_t& message); @@ -82,6 +90,9 @@ private slots: bool _currentFileValidCrc = false; QElapsedTimer _downloadStartTime; + MetadataSource _metadataSource = MetadataSource::None; + QString _metadataUri; + bool _metadataIsFallback = false; // State pointers AsyncFunctionState* _stateRequestCompInfo = nullptr; diff --git a/test/Vehicle/ComponentInformation/ComponentInformationTranslationTest.cc b/test/Vehicle/ComponentInformation/ComponentInformationTranslationTest.cc index 3d63461fb42f..13726c3b20ac 100644 --- a/test/Vehicle/ComponentInformation/ComponentInformationTranslationTest.cc +++ b/test/Vehicle/ComponentInformation/ComponentInformationTranslationTest.cc @@ -42,6 +42,12 @@ void ComponentInformationTranslationTest::_downloadAndTranslateFromSummary_test( { const QString summaryPath = tempPath(QStringLiteral("summary.json")); const QString locale = QLocale::system().name(); + + // Skip test on English locales since translation is intentionally skipped + if (locale.startsWith(QLatin1String("en"))) { + QSKIP("Translation is skipped for English locales"); + } + QFile summaryFile(summaryPath); QVERIFY(summaryFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)); QTextStream summaryStream(&summaryFile); @@ -58,7 +64,7 @@ void ComponentInformationTranslationTest::_downloadAndTranslateFromSummary_test( QSignalSpy completeSpy(&translation, &ComponentInformationTranslation::downloadComplete); QVERIFY(completeSpy.isValid()); - QVERIFY(translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600)); + QVERIFY(translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600, QStringLiteral("TEST"))); QVERIFY_SIGNAL_WAIT(completeSpy, TestTimeout::mediumMs()); QCOMPARE(completeSpy.count(), 1); @@ -93,7 +99,7 @@ void ComponentInformationTranslationTest::_downloadAndTranslateMissingLocale_tes ComponentInformationTranslation translation(this, &cachedDownloader); QSignalSpy completeSpy(&translation, &ComponentInformationTranslation::downloadComplete); - QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600)); + QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600, QStringLiteral("TEST"))); QCOMPARE(completeSpy.count(), 0); } @@ -116,7 +122,7 @@ void ComponentInformationTranslationTest::_downloadAndTranslateMissingUrl_test() ComponentInformationTranslation translation(this, &cachedDownloader); QSignalSpy completeSpy(&translation, &ComponentInformationTranslation::downloadComplete); - QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600)); + QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600, QStringLiteral("TEST"))); QCOMPARE(completeSpy.count(), 0); } @@ -132,7 +138,7 @@ void ComponentInformationTranslationTest::_downloadAndTranslateInvalidSummaryJso ComponentInformationTranslation translation(this, &cachedDownloader); QSignalSpy completeSpy(&translation, &ComponentInformationTranslation::downloadComplete); - QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600)); + QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600, QStringLiteral("TEST"))); QCOMPARE(completeSpy.count(), 0); } From ed273a3dd4cf1d3e62ff33d5bc37d0a2566400de Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 1 Mar 2026 00:33:10 +0000 Subject: [PATCH 016/119] New Crowdin translations by GitHub Action --- .../initial-connect-state-machine-analysis.md | 374 ++++++++++++++++++ docs/ko/qgc-dev-guide/command_line_options.md | 2 +- .../getting_started/download_and_install.md | 1 + .../initial-connect-state-machine-analysis.md | 374 ++++++++++++++++++ docs/tr/qgc-dev-guide/command_line_options.md | 2 +- .../getting_started/download_and_install.md | 1 + .../initial-connect-state-machine-analysis.md | 374 ++++++++++++++++++ docs/zh/qgc-dev-guide/command_line_options.md | 20 +- .../getting_started/download_and_install.md | 1 + 9 files changed, 1137 insertions(+), 12 deletions(-) create mode 100644 docs/ko/initial-connect-state-machine-analysis.md create mode 100644 docs/tr/initial-connect-state-machine-analysis.md create mode 100644 docs/zh/initial-connect-state-machine-analysis.md diff --git a/docs/ko/initial-connect-state-machine-analysis.md b/docs/ko/initial-connect-state-machine-analysis.md new file mode 100644 index 000000000000..bc2e4d216817 --- /dev/null +++ b/docs/ko/initial-connect-state-machine-analysis.md @@ -0,0 +1,374 @@ +# Initial Connect State Machine Analysis + +## Scope + +This document analyzes QGroundControl's vehicle initial connection pipeline implemented in: + +- `src/Vehicle/InitialConnectStateMachine.h` +- `src/Vehicle/InitialConnectStateMachine.cc` + +It also covers upstream/downstream dependencies that influence behavior: + +- `src/Vehicle/Vehicle.cc` +- `src/Vehicle/ComponentInformation/ComponentInformationManager.*` +- `src/Utilities/StateMachine/*` +- `src/FirmwarePlugin/FirmwarePlugin.cc` +- `src/FactSystem/ParameterManager.cc` +- `src/MissionManager/GeoFenceManager.cc` +- `src/MissionManager/RallyPointManager.cc` +- `src/API/QGCOptions.h` + +--- + +## 1. What the Initial Connect State Machine does + +At first online vehicle connection, QGC runs a linear state machine to gather critical vehicle metadata and synchronize mission-related state. + +### State order + +1. **Request AUTOPILOT_VERSION** (`RetryableRequestMessageState`) +2. **Request Standard Modes** (`AsyncFunctionState`) +3. **Request Component Information** (`AsyncFunctionState`) +4. **Request Parameters** (`AsyncFunctionState`) +5. **Request Mission** (`SkippableAsyncState`) +6. **Request Geofence** (`SkippableAsyncState`) +7. **Request Rally Points** (`SkippableAsyncState`) +8. **Signal Complete** (`RetryState` with zero retries) +9. **Final** (`QGCFinalState`) + +The state machine is started from `Vehicle` construction for normal vehicles. + +--- + +## 2. Progress model + +The machine uses weighted progress: + +- AutopilotVersion: 1 +- StandardModes: 1 +- ComponentInfo: 5 +- Parameters: 5 +- Mission: 2 +- GeoFence: 1 +- RallyPoints: 1 +- Complete: 1 + +This means parameter and component-info stages dominate progress reporting (as intended), while mission/fence/rally are lighter-weight contributors. + +Sub-progress is wired from managers during long operations: + +- `ComponentInformationManager::progressUpdate` +- `ParameterManager::loadProgressChanged` +- `MissionManager::progressPctChanged` +- `GeoFenceManager::progressPctChanged` +- `RallyPointManager::progressPctChanged` + +--- + +## 3. Retries/timeouts and failure behavior + +### Per-state retry policy + +`_maxRetries = 1` globally for initial connect states. + +Timeouts: + +- AUTOPILOT_VERSION: 5000 ms +- StandardModes: 5000 ms +- ComponentInfo: 30000 ms +- Parameters: 60000 ms +- Mission: 30000 ms +- GeoFence: 15000 ms +- RallyPoints: 15000 ms + +### Important semantic detail + +For all async states after AUTOPILOT_VERSION, timeout handling uses `RetryTransition`: + +- On first timeout: retry in-place (`restartWait()` + retry action), no state transition. +- After retries exhausted: transition to the next state. + +So timeout exhaustion is **graceful degradation** (advance), not hard stop, for this machine. + +### AUTOPILOT_VERSION state behavior + +AUTOPILOT_VERSION uses `RetryableRequestMessageState`, which also retries and then advances by default when retries are exhausted (`failOnMaxRetries` is not enabled here). It invokes a failure handler before advancing. + +--- + +## 4. Exactly what is skipped, and when + +## A) Entire machine skipped + +In unit tests, the machine is **not started** only for synthetic generic vehicles: + +- Condition: `qgcApp()->runningUnitTests() && _vehicleType == MAV_TYPE_GENERIC` + +All real vehicle types still run initial connect during tests. + +## B) AUTOPILOT_VERSION request skipped + +State skip predicate returns true if: + +1. No primary link exists, **or** +2. Primary link is high-latency (`LinkConfiguration::isHighLatency()`), **or** +3. Primary link is log replay (`LinkInterface::isLogReplay()` true, e.g. `LogReplayLink`) + +When skipped, the state completes immediately and transitions to StandardModes. + +## C) Mission load skipped + +Mission state is skipped if: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link + +## D) Geofence load skipped + +Geofence state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `GeoFenceManager::supported()` is false + - `supported()` is capability-driven: requires `MAV_PROTOCOL_CAPABILITY_MISSION_FENCE` + +## E) Rally points load skipped + +Rally state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `RallyPointManager::supported()` is false + - `supported()` requires `MAV_PROTOCOL_CAPABILITY_MISSION_RALLY` + +When rally is skipped, QGC still marks: + +- `_initialPlanRequestComplete = true` +- emits `initialPlanRequestCompleteChanged(true)` + +so downstream UI logic does not stall waiting for rally load. + +## F) Component information sub-states skipped (inside ComponentInformationManager) + +`ComponentInformationManager` itself has internal skippable phases: + +- Parameter metadata skipped if `GENERAL` metadata says parameter metadata unsupported +- Events metadata skipped if unsupported +- Actuators metadata skipped if unsupported + +Support checks are based on `CompInfoGeneral::isMetaDataTypeSupported(type)`. + +--- + +## 5. What data is captured during initial connect, and why + +## A) AUTOPILOT_VERSION data captured + +On successful AUTOPILOT_VERSION response, QGC captures: + +1. **Vehicle UID** (`uid`) + - Used as stable identity and surfaced via `vehicleUIDChanged`. + +2. **Firmware board vendor/product IDs** + - Hardware identification for board-specific behavior and diagnostics. + +3. **Flight software semantic version** (`flight_sw_version` packed fields) + - Parsed into major/minor/patch/version-type and stored in `Vehicle`. + +4. **Firmware custom version / git hash** + - PX4: decoded from binary bytes (reverse-order formatting) + - APM: decoded as 8-char ASCII + - Exposed via `gitHashChanged`. + +5. **MAVLink capability bits** + - Stored via `_setCapabilities(...)` + - Drives feature gating such as mission-int, command-int, geofence, rally support. + +If AUTOPILOT_VERSION fails, QGC sets **assumed capabilities**: + +- Always assume MAVLink2 +- For PX4/APM, also assume mission-int/command-int/fence/rally support + +This allows connect flow to continue even without that message. + +## B) Standard modes captured + +QGC requests available standard modes one-by-one (`AVAILABLE_MODES`) and updates firmware plugin mode mapping. + +Purpose: + +- Provide accurate, vehicle-reported mode names/availability. +- Refresh flight mode UI mapping even if HEARTBEAT arrives earlier. + +## C) Component Information captured + +`ComponentInformationManager` requests metadata in this sequence: + +1. `GENERAL` metadata +2. URI update pass (maps/propagates discovered metadata URIs) +3. `PARAMETER` metadata (if supported) +4. `EVENTS` metadata (if supported) +5. `ACTUATORS` metadata (if supported) + +### Why this matters + +- **Parameter metadata** (`CompInfoParam`) feeds `ParameterManager` metadata lookups (`factMetaDataForName`), affecting parameter typing/metadata behavior. +- **Events metadata** supports event schema/decoding ecosystem. +- **Actuators metadata** is used by PX4 actuator configuration UX: + - If actuator metadata is absent or actuator conditions fail, PX4 plugin falls back to legacy Motor page. + - If present and valid (`show-ui-if` etc.), Actuators page is enabled. + +So component info is both a data-quality improvement and a feature-availability gate. + +## D) Parameters stage captured + +- Requests full parameter refresh (`refreshAllParameters()`) +- Waits for `parametersReadyChanged(true)` +- On ready: + - Sends GCS time to vehicle twice (reliability) + - Sets up auto-disarm signalling + +This stage is foundational for most setup/calibration UI and many capability/behavior branches. + +## E) Plan data captured + +- Mission items from vehicle +- Geofence data (if supported and not skipped) +- Rally points (if supported and not skipped) + +On rally completion (or rally skip), QGC sets initial plan load complete. + +--- + +## 6. Where "check latest stable firmware" fits + +The latest-stable firmware check is triggered as a **side-effect** of AUTOPILOT_VERSION success: + +- Condition: `QGCOptions::checkFirmwareVersion()` is true and `_checkLatestStableFWDone` is false. +- Action: `FirmwarePlugin::checkIfIsLatestStable(vehicle)`. + +This is asynchronous (downloads and parses a version file), and **does not gate** state machine transitions. Initial connect continues regardless of result. + +It is skipped during unit tests in `FirmwarePlugin::checkIfIsLatestStable`. + +--- + +## 7. Options/configurations that affect behavior + +1. **Link configuration high-latency flag** + - `LinkConfiguration.highLatency` + - Causes skipping of AUTOPILOT_VERSION and plan-load states. + +2. **Log replay link type** + - `LinkInterface::isLogReplay()` true + - Same skip effect as high-latency. + +3. **Firmware/version check option** + - `QGCOptions::checkFirmwareVersion()` (virtual, custom-build overridable) + - Controls only latest-stable check side-effect, not transition graph. + +4. **Capability bits** + - From AUTOPILOT_VERSION or assumed fallback. + - Gate geofence/rally support and therefore skip decisions. + +5. **Metadata support flags from CompInfoGeneral** + - Gate internal component-info sub-requests (param/events/actuators metadata). + +--- + +## 8. Practical implications + +- The machine is designed for **robust connect**, not strict fail-fast. +- Network/link quality and link type strongly influence which expensive operations are skipped. +- Component information is a key bridge between firmware-reported metadata and dynamic UI behavior (especially parameters and actuators). +- Missing AUTOPILOT_VERSION does not block connect; QGC degrades gracefully with assumptions. + +--- + +## 9. Quick skip matrix + +| State | Skip trigger(s) | Outcome | +| -------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ | +| AUTOPILOT_VERSION | no primary link OR high-latency OR log replay | immediate complete/advance | +| StandardModes | none explicit | retry-on-timeout then advance | +| ComponentInfo | none explicit | retry-on-timeout then advance | +| Parameters | none explicit | retry-on-timeout then advance | +| Mission | high-latency OR log replay OR no primary link | skipped -> GeoFence | +| GeoFence | mission skip conditions OR !fence capability | skipped -> Rally | +| Rally | mission skip conditions OR !rally capability | skipped -> Complete (+mark plan complete) | +| Complete | never skipped | emits `initialConnectComplete` at machine finish | + +### Run matrix by (HighLatency, LogReplay, Flying) + +Assumptions for this matrix: + +- Primary link exists +- Geofence and rally are supported by capability bits +- State machine is started (not the `runningUnitTests && MAV_TYPE_GENERIC` bypass) + +Legend: + +- `Run` = state logic executes (it may still complete quickly) +- `Skip` = state is skipped by state skip predicate +- `Run*` = state executes but parameter download is short-circuited inside `ParameterManager` for high-latency/log-replay links + +| State | HL=0 LR=0 Fly=0 | HL=0 LR=0 Fly=1 | HL=1 LR=0 Fly=0 | HL=1 LR=0 Fly=1 | HL=0 LR=1 Fly=0 | HL=0 LR=1 Fly=1 | HL=1 LR=1 Fly=0 | HL=1 LR=1 Fly=1 | +| -------------------------------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | +| AUTOPILOT_VERSION | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| StandardModes | Run | Run | Run | Run | Run | Run | Run | Run | +| ComponentInfo | Run | Run | Run | Run | Run | Run | Run | Run | +| Parameters | Run | Run | Run\* | Run\* | Run\* | Run\* | Run\* | Run\* | +| Mission | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| GeoFence | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Rally | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Complete | Run | Run | Run | Run | Run | Run | Run | Run | + +`Flying` does not currently participate in InitialConnectStateMachine skip predicates. It does not change run/skip behavior in this machine. + +### Unit-test readable matrix (request expectations) + +This is the same compact matrix used by `_stateRunMatrix_data` in `InitialConnectTest`. + +```text ++----+----+-----+------------+------------+----------------+ +| HL | LR | Fly | AP_VERSION | AVAIL_MODS | PLAN_REQ_LISTS | ++----+----+-----+------------+------------+----------------+ +| 0 | 0 | 0 | Run | Run | Run | +| 0 | 0 | 1 | Run | Run | Run | +| 1 | 0 | 0 | Skip | Run | Skip | +| 1 | 0 | 1 | Skip | Run | Skip | +| 0 | 1 | 0 | Skip | Run | Skip | +| 0 | 1 | 1 | Skip | Run | Skip | +| 1 | 1 | 0 | Skip | Run | Skip | +| 1 | 1 | 1 | Skip | Run | Skip | ++----+----+-----+------------+------------+----------------+ +``` + +Where: + +- `HL` = HighLatency +- `LR` = LogReplay +- `Fly` = Flying +- `AP_VERSION` = `AUTOPILOT_VERSION` request expected +- `AVAIL_MODS` = `AVAILABLE_MODES` request expected +- `PLAN_REQ_LISTS` = mission/geofence/rally `MISSION_REQUEST_LIST` traffic expected + +Skip behavior basis: `skipForLinkType = (highLatency || logReplay)`. + +--- + +## 10. Notes on observability/debugging + +Useful log categories for tracing behavior: + +- `Vehicle.InitialConnectStateMachine` +- `ComponentInformation.ComponentInformationManager` +- `Vehicle.StandardModes` +- `Vehicle` (capability logs) +- `ActuatorsConfig` (for metadata-driven actuator UI decisions) + +This combination lets you determine: + +- Which states were entered/skipped/timed out +- Which metadata types were supported/downloaded +- Why actuator UI appeared or fell back to legacy motor UI diff --git a/docs/ko/qgc-dev-guide/command_line_options.md b/docs/ko/qgc-dev-guide/command_line_options.md index 040b0472cc93..6cdf89588a40 100644 --- a/docs/ko/qgc-dev-guide/command_line_options.md +++ b/docs/ko/qgc-dev-guide/command_line_options.md @@ -33,7 +33,7 @@ The options/command line arguments are listed in the table below. | Option | Description | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--clear-settings` | Clears the app settings (reverts _QGroundControl_ back to default settings). | -| `--logging:full` | Turns on full logging. See [Console Logging](../../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | +| `--logging:full` | Turns on full logging. See [Console Logging](../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | | `--logging:full,LinkManagerVerboseLog,ParameterLoaderLog` | Turns on full logging and turns off the following listed comma-separated logging options. | | `--logging:LinkManagerLog,ParameterLoaderLog` | Turns on the specified comma separated logging options | | `--unittest:name` | (Debug builds only) Runs the specified unit test. Leave off `:name` to run all tests. | diff --git a/docs/ko/qgc-user-guide/getting_started/download_and_install.md b/docs/ko/qgc-user-guide/getting_started/download_and_install.md index f82f600f1eb3..85ccf71a9099 100644 --- a/docs/ko/qgc-user-guide/getting_started/download_and_install.md +++ b/docs/ko/qgc-user-guide/getting_started/download_and_install.md @@ -78,6 +78,7 @@ sudo apt remove --purge modemmanager ```sh sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-gl -y +sudo apt install python3-gi python3-gst-1.0 -y sudo apt install libfuse2 -y sudo apt install libxcb-xinerama0 libxkbcommon-x11-0 libxcb-cursor-dev -y ``` diff --git a/docs/tr/initial-connect-state-machine-analysis.md b/docs/tr/initial-connect-state-machine-analysis.md new file mode 100644 index 000000000000..bc2e4d216817 --- /dev/null +++ b/docs/tr/initial-connect-state-machine-analysis.md @@ -0,0 +1,374 @@ +# Initial Connect State Machine Analysis + +## Scope + +This document analyzes QGroundControl's vehicle initial connection pipeline implemented in: + +- `src/Vehicle/InitialConnectStateMachine.h` +- `src/Vehicle/InitialConnectStateMachine.cc` + +It also covers upstream/downstream dependencies that influence behavior: + +- `src/Vehicle/Vehicle.cc` +- `src/Vehicle/ComponentInformation/ComponentInformationManager.*` +- `src/Utilities/StateMachine/*` +- `src/FirmwarePlugin/FirmwarePlugin.cc` +- `src/FactSystem/ParameterManager.cc` +- `src/MissionManager/GeoFenceManager.cc` +- `src/MissionManager/RallyPointManager.cc` +- `src/API/QGCOptions.h` + +--- + +## 1. What the Initial Connect State Machine does + +At first online vehicle connection, QGC runs a linear state machine to gather critical vehicle metadata and synchronize mission-related state. + +### State order + +1. **Request AUTOPILOT_VERSION** (`RetryableRequestMessageState`) +2. **Request Standard Modes** (`AsyncFunctionState`) +3. **Request Component Information** (`AsyncFunctionState`) +4. **Request Parameters** (`AsyncFunctionState`) +5. **Request Mission** (`SkippableAsyncState`) +6. **Request Geofence** (`SkippableAsyncState`) +7. **Request Rally Points** (`SkippableAsyncState`) +8. **Signal Complete** (`RetryState` with zero retries) +9. **Final** (`QGCFinalState`) + +The state machine is started from `Vehicle` construction for normal vehicles. + +--- + +## 2. Progress model + +The machine uses weighted progress: + +- AutopilotVersion: 1 +- StandardModes: 1 +- ComponentInfo: 5 +- Parameters: 5 +- Mission: 2 +- GeoFence: 1 +- RallyPoints: 1 +- Complete: 1 + +This means parameter and component-info stages dominate progress reporting (as intended), while mission/fence/rally are lighter-weight contributors. + +Sub-progress is wired from managers during long operations: + +- `ComponentInformationManager::progressUpdate` +- `ParameterManager::loadProgressChanged` +- `MissionManager::progressPctChanged` +- `GeoFenceManager::progressPctChanged` +- `RallyPointManager::progressPctChanged` + +--- + +## 3. Retries/timeouts and failure behavior + +### Per-state retry policy + +`_maxRetries = 1` globally for initial connect states. + +Timeouts: + +- AUTOPILOT_VERSION: 5000 ms +- StandardModes: 5000 ms +- ComponentInfo: 30000 ms +- Parameters: 60000 ms +- Mission: 30000 ms +- GeoFence: 15000 ms +- RallyPoints: 15000 ms + +### Important semantic detail + +For all async states after AUTOPILOT_VERSION, timeout handling uses `RetryTransition`: + +- On first timeout: retry in-place (`restartWait()` + retry action), no state transition. +- After retries exhausted: transition to the next state. + +So timeout exhaustion is **graceful degradation** (advance), not hard stop, for this machine. + +### AUTOPILOT_VERSION state behavior + +AUTOPILOT_VERSION uses `RetryableRequestMessageState`, which also retries and then advances by default when retries are exhausted (`failOnMaxRetries` is not enabled here). It invokes a failure handler before advancing. + +--- + +## 4. Exactly what is skipped, and when + +## A) Entire machine skipped + +In unit tests, the machine is **not started** only for synthetic generic vehicles: + +- Condition: `qgcApp()->runningUnitTests() && _vehicleType == MAV_TYPE_GENERIC` + +All real vehicle types still run initial connect during tests. + +## B) AUTOPILOT_VERSION request skipped + +State skip predicate returns true if: + +1. No primary link exists, **or** +2. Primary link is high-latency (`LinkConfiguration::isHighLatency()`), **or** +3. Primary link is log replay (`LinkInterface::isLogReplay()` true, e.g. `LogReplayLink`) + +When skipped, the state completes immediately and transitions to StandardModes. + +## C) Mission load skipped + +Mission state is skipped if: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link + +## D) Geofence load skipped + +Geofence state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `GeoFenceManager::supported()` is false + - `supported()` is capability-driven: requires `MAV_PROTOCOL_CAPABILITY_MISSION_FENCE` + +## E) Rally points load skipped + +Rally state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `RallyPointManager::supported()` is false + - `supported()` requires `MAV_PROTOCOL_CAPABILITY_MISSION_RALLY` + +When rally is skipped, QGC still marks: + +- `_initialPlanRequestComplete = true` +- emits `initialPlanRequestCompleteChanged(true)` + +so downstream UI logic does not stall waiting for rally load. + +## F) Component information sub-states skipped (inside ComponentInformationManager) + +`ComponentInformationManager` itself has internal skippable phases: + +- Parameter metadata skipped if `GENERAL` metadata says parameter metadata unsupported +- Events metadata skipped if unsupported +- Actuators metadata skipped if unsupported + +Support checks are based on `CompInfoGeneral::isMetaDataTypeSupported(type)`. + +--- + +## 5. What data is captured during initial connect, and why + +## A) AUTOPILOT_VERSION data captured + +On successful AUTOPILOT_VERSION response, QGC captures: + +1. **Vehicle UID** (`uid`) + - Used as stable identity and surfaced via `vehicleUIDChanged`. + +2. **Firmware board vendor/product IDs** + - Hardware identification for board-specific behavior and diagnostics. + +3. **Flight software semantic version** (`flight_sw_version` packed fields) + - Parsed into major/minor/patch/version-type and stored in `Vehicle`. + +4. **Firmware custom version / git hash** + - PX4: decoded from binary bytes (reverse-order formatting) + - APM: decoded as 8-char ASCII + - Exposed via `gitHashChanged`. + +5. **MAVLink capability bits** + - Stored via `_setCapabilities(...)` + - Drives feature gating such as mission-int, command-int, geofence, rally support. + +If AUTOPILOT_VERSION fails, QGC sets **assumed capabilities**: + +- Always assume MAVLink2 +- For PX4/APM, also assume mission-int/command-int/fence/rally support + +This allows connect flow to continue even without that message. + +## B) Standard modes captured + +QGC requests available standard modes one-by-one (`AVAILABLE_MODES`) and updates firmware plugin mode mapping. + +Purpose: + +- Provide accurate, vehicle-reported mode names/availability. +- Refresh flight mode UI mapping even if HEARTBEAT arrives earlier. + +## C) Component Information captured + +`ComponentInformationManager` requests metadata in this sequence: + +1. `GENERAL` metadata +2. URI update pass (maps/propagates discovered metadata URIs) +3. `PARAMETER` metadata (if supported) +4. `EVENTS` metadata (if supported) +5. `ACTUATORS` metadata (if supported) + +### Why this matters + +- **Parameter metadata** (`CompInfoParam`) feeds `ParameterManager` metadata lookups (`factMetaDataForName`), affecting parameter typing/metadata behavior. +- **Events metadata** supports event schema/decoding ecosystem. +- **Actuators metadata** is used by PX4 actuator configuration UX: + - If actuator metadata is absent or actuator conditions fail, PX4 plugin falls back to legacy Motor page. + - If present and valid (`show-ui-if` etc.), Actuators page is enabled. + +So component info is both a data-quality improvement and a feature-availability gate. + +## D) Parameters stage captured + +- Requests full parameter refresh (`refreshAllParameters()`) +- Waits for `parametersReadyChanged(true)` +- On ready: + - Sends GCS time to vehicle twice (reliability) + - Sets up auto-disarm signalling + +This stage is foundational for most setup/calibration UI and many capability/behavior branches. + +## E) Plan data captured + +- Mission items from vehicle +- Geofence data (if supported and not skipped) +- Rally points (if supported and not skipped) + +On rally completion (or rally skip), QGC sets initial plan load complete. + +--- + +## 6. Where "check latest stable firmware" fits + +The latest-stable firmware check is triggered as a **side-effect** of AUTOPILOT_VERSION success: + +- Condition: `QGCOptions::checkFirmwareVersion()` is true and `_checkLatestStableFWDone` is false. +- Action: `FirmwarePlugin::checkIfIsLatestStable(vehicle)`. + +This is asynchronous (downloads and parses a version file), and **does not gate** state machine transitions. Initial connect continues regardless of result. + +It is skipped during unit tests in `FirmwarePlugin::checkIfIsLatestStable`. + +--- + +## 7. Options/configurations that affect behavior + +1. **Link configuration high-latency flag** + - `LinkConfiguration.highLatency` + - Causes skipping of AUTOPILOT_VERSION and plan-load states. + +2. **Log replay link type** + - `LinkInterface::isLogReplay()` true + - Same skip effect as high-latency. + +3. **Firmware/version check option** + - `QGCOptions::checkFirmwareVersion()` (virtual, custom-build overridable) + - Controls only latest-stable check side-effect, not transition graph. + +4. **Capability bits** + - From AUTOPILOT_VERSION or assumed fallback. + - Gate geofence/rally support and therefore skip decisions. + +5. **Metadata support flags from CompInfoGeneral** + - Gate internal component-info sub-requests (param/events/actuators metadata). + +--- + +## 8. Practical implications + +- The machine is designed for **robust connect**, not strict fail-fast. +- Network/link quality and link type strongly influence which expensive operations are skipped. +- Component information is a key bridge between firmware-reported metadata and dynamic UI behavior (especially parameters and actuators). +- Missing AUTOPILOT_VERSION does not block connect; QGC degrades gracefully with assumptions. + +--- + +## 9. Quick skip matrix + +| State | Skip trigger(s) | Outcome | +| -------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ | +| AUTOPILOT_VERSION | no primary link OR high-latency OR log replay | immediate complete/advance | +| StandardModes | none explicit | retry-on-timeout then advance | +| ComponentInfo | none explicit | retry-on-timeout then advance | +| Parameters | none explicit | retry-on-timeout then advance | +| Mission | high-latency OR log replay OR no primary link | skipped -> GeoFence | +| GeoFence | mission skip conditions OR !fence capability | skipped -> Rally | +| Rally | mission skip conditions OR !rally capability | skipped -> Complete (+mark plan complete) | +| Complete | never skipped | emits `initialConnectComplete` at machine finish | + +### Run matrix by (HighLatency, LogReplay, Flying) + +Assumptions for this matrix: + +- Primary link exists +- Geofence and rally are supported by capability bits +- State machine is started (not the `runningUnitTests && MAV_TYPE_GENERIC` bypass) + +Legend: + +- `Run` = state logic executes (it may still complete quickly) +- `Skip` = state is skipped by state skip predicate +- `Run*` = state executes but parameter download is short-circuited inside `ParameterManager` for high-latency/log-replay links + +| State | HL=0 LR=0 Fly=0 | HL=0 LR=0 Fly=1 | HL=1 LR=0 Fly=0 | HL=1 LR=0 Fly=1 | HL=0 LR=1 Fly=0 | HL=0 LR=1 Fly=1 | HL=1 LR=1 Fly=0 | HL=1 LR=1 Fly=1 | +| -------------------------------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | +| AUTOPILOT_VERSION | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| StandardModes | Run | Run | Run | Run | Run | Run | Run | Run | +| ComponentInfo | Run | Run | Run | Run | Run | Run | Run | Run | +| Parameters | Run | Run | Run\* | Run\* | Run\* | Run\* | Run\* | Run\* | +| Mission | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| GeoFence | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Rally | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Complete | Run | Run | Run | Run | Run | Run | Run | Run | + +`Flying` does not currently participate in InitialConnectStateMachine skip predicates. It does not change run/skip behavior in this machine. + +### Unit-test readable matrix (request expectations) + +This is the same compact matrix used by `_stateRunMatrix_data` in `InitialConnectTest`. + +```text ++----+----+-----+------------+------------+----------------+ +| HL | LR | Fly | AP_VERSION | AVAIL_MODS | PLAN_REQ_LISTS | ++----+----+-----+------------+------------+----------------+ +| 0 | 0 | 0 | Run | Run | Run | +| 0 | 0 | 1 | Run | Run | Run | +| 1 | 0 | 0 | Skip | Run | Skip | +| 1 | 0 | 1 | Skip | Run | Skip | +| 0 | 1 | 0 | Skip | Run | Skip | +| 0 | 1 | 1 | Skip | Run | Skip | +| 1 | 1 | 0 | Skip | Run | Skip | +| 1 | 1 | 1 | Skip | Run | Skip | ++----+----+-----+------------+------------+----------------+ +``` + +Where: + +- `HL` = HighLatency +- `LR` = LogReplay +- `Fly` = Flying +- `AP_VERSION` = `AUTOPILOT_VERSION` request expected +- `AVAIL_MODS` = `AVAILABLE_MODES` request expected +- `PLAN_REQ_LISTS` = mission/geofence/rally `MISSION_REQUEST_LIST` traffic expected + +Skip behavior basis: `skipForLinkType = (highLatency || logReplay)`. + +--- + +## 10. Notes on observability/debugging + +Useful log categories for tracing behavior: + +- `Vehicle.InitialConnectStateMachine` +- `ComponentInformation.ComponentInformationManager` +- `Vehicle.StandardModes` +- `Vehicle` (capability logs) +- `ActuatorsConfig` (for metadata-driven actuator UI decisions) + +This combination lets you determine: + +- Which states were entered/skipped/timed out +- Which metadata types were supported/downloaded +- Why actuator UI appeared or fell back to legacy motor UI diff --git a/docs/tr/qgc-dev-guide/command_line_options.md b/docs/tr/qgc-dev-guide/command_line_options.md index 040b0472cc93..6cdf89588a40 100644 --- a/docs/tr/qgc-dev-guide/command_line_options.md +++ b/docs/tr/qgc-dev-guide/command_line_options.md @@ -33,7 +33,7 @@ The options/command line arguments are listed in the table below. | Option | Description | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--clear-settings` | Clears the app settings (reverts _QGroundControl_ back to default settings). | -| `--logging:full` | Turns on full logging. See [Console Logging](../../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | +| `--logging:full` | Turns on full logging. See [Console Logging](../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | | `--logging:full,LinkManagerVerboseLog,ParameterLoaderLog` | Turns on full logging and turns off the following listed comma-separated logging options. | | `--logging:LinkManagerLog,ParameterLoaderLog` | Turns on the specified comma separated logging options | | `--unittest:name` | (Debug builds only) Runs the specified unit test. Leave off `:name` to run all tests. | diff --git a/docs/tr/qgc-user-guide/getting_started/download_and_install.md b/docs/tr/qgc-user-guide/getting_started/download_and_install.md index 7ca6f7148394..28289aabc6cc 100644 --- a/docs/tr/qgc-user-guide/getting_started/download_and_install.md +++ b/docs/tr/qgc-user-guide/getting_started/download_and_install.md @@ -78,6 +78,7 @@ sudo apt remove --purge modemmanager ```sh sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-gl -y +sudo apt install python3-gi python3-gst-1.0 -y sudo apt install libfuse2 -y sudo apt install libxcb-xinerama0 libxkbcommon-x11-0 libxcb-cursor-dev -y ``` diff --git a/docs/zh/initial-connect-state-machine-analysis.md b/docs/zh/initial-connect-state-machine-analysis.md new file mode 100644 index 000000000000..bc2e4d216817 --- /dev/null +++ b/docs/zh/initial-connect-state-machine-analysis.md @@ -0,0 +1,374 @@ +# Initial Connect State Machine Analysis + +## Scope + +This document analyzes QGroundControl's vehicle initial connection pipeline implemented in: + +- `src/Vehicle/InitialConnectStateMachine.h` +- `src/Vehicle/InitialConnectStateMachine.cc` + +It also covers upstream/downstream dependencies that influence behavior: + +- `src/Vehicle/Vehicle.cc` +- `src/Vehicle/ComponentInformation/ComponentInformationManager.*` +- `src/Utilities/StateMachine/*` +- `src/FirmwarePlugin/FirmwarePlugin.cc` +- `src/FactSystem/ParameterManager.cc` +- `src/MissionManager/GeoFenceManager.cc` +- `src/MissionManager/RallyPointManager.cc` +- `src/API/QGCOptions.h` + +--- + +## 1. What the Initial Connect State Machine does + +At first online vehicle connection, QGC runs a linear state machine to gather critical vehicle metadata and synchronize mission-related state. + +### State order + +1. **Request AUTOPILOT_VERSION** (`RetryableRequestMessageState`) +2. **Request Standard Modes** (`AsyncFunctionState`) +3. **Request Component Information** (`AsyncFunctionState`) +4. **Request Parameters** (`AsyncFunctionState`) +5. **Request Mission** (`SkippableAsyncState`) +6. **Request Geofence** (`SkippableAsyncState`) +7. **Request Rally Points** (`SkippableAsyncState`) +8. **Signal Complete** (`RetryState` with zero retries) +9. **Final** (`QGCFinalState`) + +The state machine is started from `Vehicle` construction for normal vehicles. + +--- + +## 2. Progress model + +The machine uses weighted progress: + +- AutopilotVersion: 1 +- StandardModes: 1 +- ComponentInfo: 5 +- Parameters: 5 +- Mission: 2 +- GeoFence: 1 +- RallyPoints: 1 +- Complete: 1 + +This means parameter and component-info stages dominate progress reporting (as intended), while mission/fence/rally are lighter-weight contributors. + +Sub-progress is wired from managers during long operations: + +- `ComponentInformationManager::progressUpdate` +- `ParameterManager::loadProgressChanged` +- `MissionManager::progressPctChanged` +- `GeoFenceManager::progressPctChanged` +- `RallyPointManager::progressPctChanged` + +--- + +## 3. Retries/timeouts and failure behavior + +### Per-state retry policy + +`_maxRetries = 1` globally for initial connect states. + +Timeouts: + +- AUTOPILOT_VERSION: 5000 ms +- StandardModes: 5000 ms +- ComponentInfo: 30000 ms +- Parameters: 60000 ms +- Mission: 30000 ms +- GeoFence: 15000 ms +- RallyPoints: 15000 ms + +### Important semantic detail + +For all async states after AUTOPILOT_VERSION, timeout handling uses `RetryTransition`: + +- On first timeout: retry in-place (`restartWait()` + retry action), no state transition. +- After retries exhausted: transition to the next state. + +So timeout exhaustion is **graceful degradation** (advance), not hard stop, for this machine. + +### AUTOPILOT_VERSION state behavior + +AUTOPILOT_VERSION uses `RetryableRequestMessageState`, which also retries and then advances by default when retries are exhausted (`failOnMaxRetries` is not enabled here). It invokes a failure handler before advancing. + +--- + +## 4. Exactly what is skipped, and when + +## A) Entire machine skipped + +In unit tests, the machine is **not started** only for synthetic generic vehicles: + +- Condition: `qgcApp()->runningUnitTests() && _vehicleType == MAV_TYPE_GENERIC` + +All real vehicle types still run initial connect during tests. + +## B) AUTOPILOT_VERSION request skipped + +State skip predicate returns true if: + +1. No primary link exists, **or** +2. Primary link is high-latency (`LinkConfiguration::isHighLatency()`), **or** +3. Primary link is log replay (`LinkInterface::isLogReplay()` true, e.g. `LogReplayLink`) + +When skipped, the state completes immediately and transitions to StandardModes. + +## C) Mission load skipped + +Mission state is skipped if: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link + +## D) Geofence load skipped + +Geofence state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `GeoFenceManager::supported()` is false + - `supported()` is capability-driven: requires `MAV_PROTOCOL_CAPABILITY_MISSION_FENCE` + +## E) Rally points load skipped + +Rally state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `RallyPointManager::supported()` is false + - `supported()` requires `MAV_PROTOCOL_CAPABILITY_MISSION_RALLY` + +When rally is skipped, QGC still marks: + +- `_initialPlanRequestComplete = true` +- emits `initialPlanRequestCompleteChanged(true)` + +so downstream UI logic does not stall waiting for rally load. + +## F) Component information sub-states skipped (inside ComponentInformationManager) + +`ComponentInformationManager` itself has internal skippable phases: + +- Parameter metadata skipped if `GENERAL` metadata says parameter metadata unsupported +- Events metadata skipped if unsupported +- Actuators metadata skipped if unsupported + +Support checks are based on `CompInfoGeneral::isMetaDataTypeSupported(type)`. + +--- + +## 5. What data is captured during initial connect, and why + +## A) AUTOPILOT_VERSION data captured + +On successful AUTOPILOT_VERSION response, QGC captures: + +1. **Vehicle UID** (`uid`) + - Used as stable identity and surfaced via `vehicleUIDChanged`. + +2. **Firmware board vendor/product IDs** + - Hardware identification for board-specific behavior and diagnostics. + +3. **Flight software semantic version** (`flight_sw_version` packed fields) + - Parsed into major/minor/patch/version-type and stored in `Vehicle`. + +4. **Firmware custom version / git hash** + - PX4: decoded from binary bytes (reverse-order formatting) + - APM: decoded as 8-char ASCII + - Exposed via `gitHashChanged`. + +5. **MAVLink capability bits** + - Stored via `_setCapabilities(...)` + - Drives feature gating such as mission-int, command-int, geofence, rally support. + +If AUTOPILOT_VERSION fails, QGC sets **assumed capabilities**: + +- Always assume MAVLink2 +- For PX4/APM, also assume mission-int/command-int/fence/rally support + +This allows connect flow to continue even without that message. + +## B) Standard modes captured + +QGC requests available standard modes one-by-one (`AVAILABLE_MODES`) and updates firmware plugin mode mapping. + +Purpose: + +- Provide accurate, vehicle-reported mode names/availability. +- Refresh flight mode UI mapping even if HEARTBEAT arrives earlier. + +## C) Component Information captured + +`ComponentInformationManager` requests metadata in this sequence: + +1. `GENERAL` metadata +2. URI update pass (maps/propagates discovered metadata URIs) +3. `PARAMETER` metadata (if supported) +4. `EVENTS` metadata (if supported) +5. `ACTUATORS` metadata (if supported) + +### Why this matters + +- **Parameter metadata** (`CompInfoParam`) feeds `ParameterManager` metadata lookups (`factMetaDataForName`), affecting parameter typing/metadata behavior. +- **Events metadata** supports event schema/decoding ecosystem. +- **Actuators metadata** is used by PX4 actuator configuration UX: + - If actuator metadata is absent or actuator conditions fail, PX4 plugin falls back to legacy Motor page. + - If present and valid (`show-ui-if` etc.), Actuators page is enabled. + +So component info is both a data-quality improvement and a feature-availability gate. + +## D) Parameters stage captured + +- Requests full parameter refresh (`refreshAllParameters()`) +- Waits for `parametersReadyChanged(true)` +- On ready: + - Sends GCS time to vehicle twice (reliability) + - Sets up auto-disarm signalling + +This stage is foundational for most setup/calibration UI and many capability/behavior branches. + +## E) Plan data captured + +- Mission items from vehicle +- Geofence data (if supported and not skipped) +- Rally points (if supported and not skipped) + +On rally completion (or rally skip), QGC sets initial plan load complete. + +--- + +## 6. Where "check latest stable firmware" fits + +The latest-stable firmware check is triggered as a **side-effect** of AUTOPILOT_VERSION success: + +- Condition: `QGCOptions::checkFirmwareVersion()` is true and `_checkLatestStableFWDone` is false. +- Action: `FirmwarePlugin::checkIfIsLatestStable(vehicle)`. + +This is asynchronous (downloads and parses a version file), and **does not gate** state machine transitions. Initial connect continues regardless of result. + +It is skipped during unit tests in `FirmwarePlugin::checkIfIsLatestStable`. + +--- + +## 7. Options/configurations that affect behavior + +1. **Link configuration high-latency flag** + - `LinkConfiguration.highLatency` + - Causes skipping of AUTOPILOT_VERSION and plan-load states. + +2. **Log replay link type** + - `LinkInterface::isLogReplay()` true + - Same skip effect as high-latency. + +3. **Firmware/version check option** + - `QGCOptions::checkFirmwareVersion()` (virtual, custom-build overridable) + - Controls only latest-stable check side-effect, not transition graph. + +4. **Capability bits** + - From AUTOPILOT_VERSION or assumed fallback. + - Gate geofence/rally support and therefore skip decisions. + +5. **Metadata support flags from CompInfoGeneral** + - Gate internal component-info sub-requests (param/events/actuators metadata). + +--- + +## 8. Practical implications + +- The machine is designed for **robust connect**, not strict fail-fast. +- Network/link quality and link type strongly influence which expensive operations are skipped. +- Component information is a key bridge between firmware-reported metadata and dynamic UI behavior (especially parameters and actuators). +- Missing AUTOPILOT_VERSION does not block connect; QGC degrades gracefully with assumptions. + +--- + +## 9. Quick skip matrix + +| State | Skip trigger(s) | Outcome | +| -------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ | +| AUTOPILOT_VERSION | no primary link OR high-latency OR log replay | immediate complete/advance | +| StandardModes | none explicit | retry-on-timeout then advance | +| ComponentInfo | none explicit | retry-on-timeout then advance | +| Parameters | none explicit | retry-on-timeout then advance | +| Mission | high-latency OR log replay OR no primary link | skipped -> GeoFence | +| GeoFence | mission skip conditions OR !fence capability | skipped -> Rally | +| Rally | mission skip conditions OR !rally capability | skipped -> Complete (+mark plan complete) | +| Complete | never skipped | emits `initialConnectComplete` at machine finish | + +### Run matrix by (HighLatency, LogReplay, Flying) + +Assumptions for this matrix: + +- Primary link exists +- Geofence and rally are supported by capability bits +- State machine is started (not the `runningUnitTests && MAV_TYPE_GENERIC` bypass) + +Legend: + +- `Run` = state logic executes (it may still complete quickly) +- `Skip` = state is skipped by state skip predicate +- `Run*` = state executes but parameter download is short-circuited inside `ParameterManager` for high-latency/log-replay links + +| State | HL=0 LR=0 Fly=0 | HL=0 LR=0 Fly=1 | HL=1 LR=0 Fly=0 | HL=1 LR=0 Fly=1 | HL=0 LR=1 Fly=0 | HL=0 LR=1 Fly=1 | HL=1 LR=1 Fly=0 | HL=1 LR=1 Fly=1 | +| -------------------------------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | +| AUTOPILOT_VERSION | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| StandardModes | Run | Run | Run | Run | Run | Run | Run | Run | +| ComponentInfo | Run | Run | Run | Run | Run | Run | Run | Run | +| Parameters | Run | Run | Run\* | Run\* | Run\* | Run\* | Run\* | Run\* | +| Mission | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| GeoFence | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Rally | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Complete | Run | Run | Run | Run | Run | Run | Run | Run | + +`Flying` does not currently participate in InitialConnectStateMachine skip predicates. It does not change run/skip behavior in this machine. + +### Unit-test readable matrix (request expectations) + +This is the same compact matrix used by `_stateRunMatrix_data` in `InitialConnectTest`. + +```text ++----+----+-----+------------+------------+----------------+ +| HL | LR | Fly | AP_VERSION | AVAIL_MODS | PLAN_REQ_LISTS | ++----+----+-----+------------+------------+----------------+ +| 0 | 0 | 0 | Run | Run | Run | +| 0 | 0 | 1 | Run | Run | Run | +| 1 | 0 | 0 | Skip | Run | Skip | +| 1 | 0 | 1 | Skip | Run | Skip | +| 0 | 1 | 0 | Skip | Run | Skip | +| 0 | 1 | 1 | Skip | Run | Skip | +| 1 | 1 | 0 | Skip | Run | Skip | +| 1 | 1 | 1 | Skip | Run | Skip | ++----+----+-----+------------+------------+----------------+ +``` + +Where: + +- `HL` = HighLatency +- `LR` = LogReplay +- `Fly` = Flying +- `AP_VERSION` = `AUTOPILOT_VERSION` request expected +- `AVAIL_MODS` = `AVAILABLE_MODES` request expected +- `PLAN_REQ_LISTS` = mission/geofence/rally `MISSION_REQUEST_LIST` traffic expected + +Skip behavior basis: `skipForLinkType = (highLatency || logReplay)`. + +--- + +## 10. Notes on observability/debugging + +Useful log categories for tracing behavior: + +- `Vehicle.InitialConnectStateMachine` +- `ComponentInformation.ComponentInformationManager` +- `Vehicle.StandardModes` +- `Vehicle` (capability logs) +- `ActuatorsConfig` (for metadata-driven actuator UI decisions) + +This combination lets you determine: + +- Which states were entered/skipped/timed out +- Which metadata types were supported/downloaded +- Why actuator UI appeared or fell back to legacy motor UI diff --git a/docs/zh/qgc-dev-guide/command_line_options.md b/docs/zh/qgc-dev-guide/command_line_options.md index dd4bb55d774a..02cf2ea7862c 100644 --- a/docs/zh/qgc-dev-guide/command_line_options.md +++ b/docs/zh/qgc-dev-guide/command_line_options.md @@ -30,16 +30,16 @@ Linux终端: 选项/命令行参数列在下表中。 -| 选项 | 描述 | -| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `--clear-settings` | Clears the app settings (reverts _QGroundControl_ back to default settings). | -| `--logging:full` | Turns on full logging. See [Console Logging](../../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | -| `--logging:full,LinkManagerVerboseLog,ParameterLoaderLog` | Turns on full logging and turns off the following listed comma-separated logging options. | -| `--logging:LinkManagerLog,ParameterLoaderLog` | Turns on the specified comma separated logging options | -| `--unittest:name` | (仅限调试版本)连续运行指定的单元测试次。 Leave off `:name` to run all tests. | -| `--unittest-stress:name` | (仅限调试版本)连续运行指定的单元测试20次。 Leave off :name to run all tests. | -| `--fake-mobile` | Simulates running on a mobile device. | -| `--test-high-dpi` | Simulates running _QGroundControl_ on a high DPI device. | +| 选项 | 描述 | +| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--clear-settings` | Clears the app settings (reverts _QGroundControl_ back to default settings). | +| `--logging:full` | Turns on full logging. See [Console Logging](../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | +| `--logging:full,LinkManagerVerboseLog,ParameterLoaderLog` | Turns on full logging and turns off the following listed comma-separated logging options. | +| `--logging:LinkManagerLog,ParameterLoaderLog` | Turns on the specified comma separated logging options | +| `--unittest:name` | (仅限调试版本)连续运行指定的单元测试次。 Leave off `:name` to run all tests. | +| `--unittest-stress:name` | (仅限调试版本)连续运行指定的单元测试20次。 Leave off :name to run all tests. | +| `--fake-mobile` | Simulates running on a mobile device. | +| `--test-high-dpi` | Simulates running _QGroundControl_ on a high DPI device. | 笔记: diff --git a/docs/zh/qgc-user-guide/getting_started/download_and_install.md b/docs/zh/qgc-user-guide/getting_started/download_and_install.md index 392a49da155b..057bfb4e2751 100644 --- a/docs/zh/qgc-user-guide/getting_started/download_and_install.md +++ b/docs/zh/qgc-user-guide/getting_started/download_and_install.md @@ -78,6 +78,7 @@ sudo apt remove --purge modemmanager ```sh sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-gl -y +sudo apt install python3-gi python3-gst-1.0 -y sudo apt install libfuse2 -y sudo apt install libxcb-xinerama0 libxkbcommon-x11-0 libxcb-cursor-dev -y ``` From 350410ef30f81f93aaf5df7a97115076ecf476d8 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 5 Mar 2026 07:36:19 -0800 Subject: [PATCH 017/119] FlightMap: zoom mouse wheel around cursor position Change WheelHandler to zoom around the current mouse cursor position instead of the map center. Uses alignCoordinateToPoint to keep the coordinate under the cursor fixed, matching the existing PinchHandler behavior. Reset _lastRotation on WheelHandler deactivation to prevent zoom jumps when rotation resets to 0 between wheel sequences. --- src/FlightMap/FlightMap.qml | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/FlightMap/FlightMap.qml b/src/FlightMap/FlightMap.qml index 0786c2e174e0..264b33951956 100644 --- a/src/FlightMap/FlightMap.qml +++ b/src/FlightMap/FlightMap.qml @@ -135,14 +135,30 @@ Map { } WheelHandler { + id: wheelHandler // workaround for QTBUG-87646 / QTBUG-112394 / QTBUG-112432: // Magic Mouse pretends to be a trackpad but doesn't work with PinchHandler // and we don't yet distinguish mice and trackpads on Wayland either - acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland" ? - PointerDevice.Mouse | PointerDevice.TouchPad : PointerDevice.Mouse - rotationScale: 1 / 120 - property: "zoomLevel" + acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland" ? PointerDevice.Mouse | PointerDevice.TouchPad : PointerDevice.Mouse + rotationScale: 1 / 120 + target: null + + property real _lastRotation: 0 + onActiveChanged: { + if (!active) { + _lastRotation = 0 + } + } + + onRotationChanged: { + let delta = rotation - _lastRotation + _lastRotation = rotation + let mousePos = wheelHandler.point.position + let coord = _map.toCoordinate(mousePos, false) + _map.zoomLevel = Math.max(_map.zoomLevel + delta, 0) + _map.alignCoordinateToPoint(coord, mousePos) + } } // We specifically do not use a DragHandler for panning. It just causes too many problems if you overlay anything else like a Flickable above it. From 9cf96520b41e8d4d316ba00489c4c0db1945d478 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 5 Mar 2026 15:56:29 -0800 Subject: [PATCH 018/119] Revert "FlightMap: zoom mouse wheel around cursor position" This reverts commit 350410ef30f81f93aaf5df7a97115076ecf476d8. --- src/FlightMap/FlightMap.qml | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/FlightMap/FlightMap.qml b/src/FlightMap/FlightMap.qml index 264b33951956..0786c2e174e0 100644 --- a/src/FlightMap/FlightMap.qml +++ b/src/FlightMap/FlightMap.qml @@ -135,30 +135,14 @@ Map { } WheelHandler { - id: wheelHandler // workaround for QTBUG-87646 / QTBUG-112394 / QTBUG-112432: // Magic Mouse pretends to be a trackpad but doesn't work with PinchHandler // and we don't yet distinguish mice and trackpads on Wayland either - acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland" ? PointerDevice.Mouse | PointerDevice.TouchPad : PointerDevice.Mouse - rotationScale: 1 / 120 - target: null - - property real _lastRotation: 0 + acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland" ? + PointerDevice.Mouse | PointerDevice.TouchPad : PointerDevice.Mouse + rotationScale: 1 / 120 + property: "zoomLevel" - onActiveChanged: { - if (!active) { - _lastRotation = 0 - } - } - - onRotationChanged: { - let delta = rotation - _lastRotation - _lastRotation = rotation - let mousePos = wheelHandler.point.position - let coord = _map.toCoordinate(mousePos, false) - _map.zoomLevel = Math.max(_map.zoomLevel + delta, 0) - _map.alignCoordinateToPoint(coord, mousePos) - } } // We specifically do not use a DragHandler for panning. It just causes too many problems if you overlay anything else like a Flickable above it. From 29c73567392e64b14ef23f94caf0178fc4cdd586 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 5 Mar 2026 10:11:39 -0800 Subject: [PATCH 019/119] fix: update shields.io release badge to current endpoint The old /github/release/ endpoint is deprecated and returns 'invalid'. Update to /github/v/release/ which correctly resolves the latest release. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 78788ca279c6..1087445ee051 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- Latest Release + Latest Release

From 0e18681b472ed9f0017505b6bd14350e96d7cb55 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 5 Mar 2026 08:56:09 -0800 Subject: [PATCH 020/119] RC Calibration: improve error messages for insufficient channels Provide distinct error messages for joystick vs RC transmitter when the channel count is below the minimum required: - Joystick mode: show stick count vs required count - RC transmitter mode: prompt to turn on transmitter when 0 channels, otherwise show required channel count - Use clearer dialog titles for each case --- .../VehicleSetup/RemoteControlCalibration.qml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Vehicle/VehicleSetup/RemoteControlCalibration.qml b/src/Vehicle/VehicleSetup/RemoteControlCalibration.qml index 388a48044df3..c76c9673ac13 100644 --- a/src/Vehicle/VehicleSetup/RemoteControlCalibration.qml +++ b/src/Vehicle/VehicleSetup/RemoteControlCalibration.qml @@ -228,11 +228,16 @@ ColumnLayout { onClicked: { if (text === qsTr("Calibrate")) { if (controller.channelCount < controller.minChannelCount) { - QGroundControl.showMessageDialog(root, qsTr("Remote Not Ready"), - controller.channelCount == 0 ? qsTr("Please turn on remote.") : - (controller.channelCount < controller.minChannelCount ? - qsTr("%1 channels or more are needed to fly.").arg(controller.minChannelCount) : - qsTr("Ready to calibrate."))) + let errorMessage = "" + let title = "" + if (controller.joystickMode) { + title = qsTr("Joystick Not Ready") + errorMessage = qsTr("%1 axes or more are needed to fly. Joystick is reporting %2 axes.").arg(controller.minChannelCount).arg(controller.channelCount) + } else { + title = qsTr("Not Ready") + errorMessage = controller.channelCount === 0 ? qsTr("Please turn on RC transmitter.") : qsTr("%1 channels or more are needed to fly.").arg(controller.minChannelCount) + } + QGroundControl.showMessageDialog(root, title, errorMessage) return } else if (!controller.joystickMode) { QGroundControl.showMessageDialog(root, qsTr("Zero Trims"), From 1609ce57a8499b6c04d3387fcb3dc25fa47d6f69 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 27 Feb 2026 12:24:49 -0800 Subject: [PATCH 021/119] MockLink: Add per-message-ID no-response injection for testing Add setRequestMessageNoResponse() API that allows tests to silently drop REQUEST_MESSAGE responses for specific MAVLink message IDs (no ACK, no message). This enables precise timeout testing of individual InitialConnectStateMachine states without affecting other message flows. - New QSet member tracks blocked message IDs - Early return in _handleRequestMessage() when ID is in the blocked set - Complements the existing RequestMessageFailureMode which is global --- src/Comms/MockLink/MockLink.cc | 8 ++++++++ src/Comms/MockLink/MockLink.h | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/Comms/MockLink/MockLink.cc b/src/Comms/MockLink/MockLink.cc index 8726262e4bc4..59b0e8f5a378 100644 --- a/src/Comms/MockLink/MockLink.cc +++ b/src/Comms/MockLink/MockLink.cc @@ -1982,6 +1982,14 @@ void MockLink::_handleRequestMessage(const mavlink_command_long_t &request, bool accepted = false; noAck = false; + const uint32_t requestedMessageId = static_cast(request.param1); + + // Per-message-ID no-response injection: silently drop the request (no ACK, no message) + if (_requestMessageNoResponseIds.contains(requestedMessageId)) { + noAck = true; + return; + } + switch (static_cast(request.param1)) { case MAVLINK_MSG_ID_AUTOPILOT_VERSION: _handleRequestMessageAutopilotVersion(request, accepted); diff --git a/src/Comms/MockLink/MockLink.h b/src/Comms/MockLink/MockLink.h index 89ef26b69e10..5643dffe7799 100644 --- a/src/Comms/MockLink/MockLink.h +++ b/src/Comms/MockLink/MockLink.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -95,6 +96,16 @@ class MockLink : public LinkInterface }; void setRequestMessageFailureMode(RequestMessageFailureMode_t failureMode) { _requestMessageFailureMode = failureMode; } + /// Block or unblock REQUEST_MESSAGE responses for a specific message ID. + /// When blocked, MockLink silently drops the request (no ACK, no message). + void setRequestMessageNoResponse(uint32_t messageId, bool noResponse = true) { + if (noResponse) { + _requestMessageNoResponseIds.insert(messageId); + } else { + _requestMessageNoResponseIds.remove(messageId); + } + } + enum ParamSetFailureMode_t { FailParamSetNone, ///< Normal behavior FailParamSetNoAck, ///< Do not send PARAM_VALUE ack @@ -308,6 +319,7 @@ private slots: QMutex _logDownloadMutex; RequestMessageFailureMode_t _requestMessageFailureMode = FailRequestMessageNone; + QSet _requestMessageNoResponseIds; ParamSetFailureMode_t _paramSetFailureMode = FailParamSetNone; bool _paramSetFailureFirstAttemptPending = false; ParamRequestReadFailureMode_t _paramRequestReadFailureMode = FailParamRequestReadNone; From bf9ca8478d9bf2e82d300c84e7ab81d9d985e0d6 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 27 Feb 2026 12:25:02 -0800 Subject: [PATCH 022/119] QGCLogging: Add test log capture infrastructure Add a thread-safe log capture system that records messages by category and severity level during unit test execution. This enables tests to assert on logged warnings, criticals, and uncategorized messages. - CapturedLogMessage struct holds type, category, and raw message text - Atomic bool gate + mutex-protected QList for minimal overhead when disabled - captureIfEnabled() public static for external handler wrappers - Query API: capturedMessages(), hasCapturedMessage(), hasCapturedWarning(), hasCapturedCritical(), hasCapturedUncategorizedMessage() - Capture call placed inside the qt.quick filter in msgHandler --- src/Utilities/QGCLogging.cc | 92 +++++++++++++++++++++++++++++++++++++ src/Utilities/QGCLogging.h | 43 +++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/src/Utilities/QGCLogging.cc b/src/Utilities/QGCLogging.cc index 442a8f64af15..3aaca642ab62 100644 --- a/src/Utilities/QGCLogging.cc +++ b/src/Utilities/QGCLogging.cc @@ -6,15 +6,26 @@ #include #include +#include +#include #include #include +#include + QGC_LOGGING_CATEGORY(QGCLoggingLog, "Utilities.QGCLogging") Q_GLOBAL_STATIC(QGCLogging, _qgcLogging) static QtMessageHandler defaultHandler = nullptr; +// --------------------------------------------------------------------------- +// Test‐log‐capture storage (thread‐safe, lightweight when disabled) +// --------------------------------------------------------------------------- +static std::atomic s_captureEnabled{false}; +static QMutex s_captureMutex; +static QList s_capturedMessages; + static void msgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { // Call the previous handler FIRST to ensure QTest::ignoreMessage works correctly. @@ -29,6 +40,9 @@ static void msgHandler(QtMsgType type, const QMessageLogContext &context, const // Filter out Qt Quick internals if (QGCLogging::instance() && !QString(context.category).startsWith("qt.quick")) { + // Capture for unit-test introspection + QGCLogging::captureIfEnabled(type, context, msg); + QGCLogging::instance()->log(message); } } @@ -71,6 +85,84 @@ void QGCLogging::installHandler() defaultHandler = qInstallMessageHandler(msgHandler); } +// --------------------------------------------------------------------------- +// Test‑log‑capture API +// --------------------------------------------------------------------------- + +void QGCLogging::setCaptureEnabled(bool enabled) +{ + s_captureEnabled.store(enabled, std::memory_order_relaxed); +} + +void QGCLogging::clearCapturedMessages() +{ + const QMutexLocker locker(&s_captureMutex); + s_capturedMessages.clear(); +} + +QList QGCLogging::capturedMessages(const QString &category) +{ + const QMutexLocker locker(&s_captureMutex); + + if (category.isEmpty()) { + return s_capturedMessages; + } + + QList filtered; + for (const auto &msg : std::as_const(s_capturedMessages)) { + if (msg.category == category) { + filtered.append(msg); + } + } + return filtered; +} + +bool QGCLogging::hasCapturedMessage(const QString &category, QtMsgType type) +{ + const QMutexLocker locker(&s_captureMutex); + + for (const auto &msg : std::as_const(s_capturedMessages)) { + if (msg.category == category && msg.type == type) { + return true; + } + } + return false; +} + +bool QGCLogging::hasCapturedWarning(const QString &category) +{ + return hasCapturedMessage(category, QtWarningMsg); +} + +bool QGCLogging::hasCapturedCritical(const QString &category) +{ + return hasCapturedMessage(category, QtCriticalMsg); +} + +bool QGCLogging::hasCapturedUncategorizedMessage() +{ + const QMutexLocker locker(&s_captureMutex); + + for (const auto &msg : std::as_const(s_capturedMessages)) { + if (msg.category.isEmpty() || msg.category == QStringLiteral("default")) { + return true; + } + } + return false; +} + +void QGCLogging::captureIfEnabled(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + if (!s_captureEnabled.load(std::memory_order_relaxed)) { + return; + } + + const QMutexLocker locker(&s_captureMutex); + s_capturedMessages.append({type, + context.category ? QString::fromLatin1(context.category) : QString(), + msg}); +} + void QGCLogging::log(const QString &message) { // Emit the signal so threadsafeLog runs in the correct thread diff --git a/src/Utilities/QGCLogging.h b/src/Utilities/QGCLogging.h index 9262c473c141..74af2e481f76 100644 --- a/src/Utilities/QGCLogging.h +++ b/src/Utilities/QGCLogging.h @@ -1,12 +1,22 @@ #pragma once #include +#include #include +#include #include #include Q_DECLARE_LOGGING_CATEGORY(QGCLoggingLog) +/// A single captured log message for test introspection. +struct CapturedLogMessage +{ + QtMsgType type = QtDebugMsg; + QString category; + QString message; +}; + class QGCLogging : public QStringListModel { Q_OBJECT @@ -27,6 +37,39 @@ class QGCLogging : public QStringListModel /// Enqueue a log message (thread-safe) void log(const QString &message); + // ======================================================================== + // Test Log Capture + // ======================================================================== + + /// Enable or disable log capture (for unit testing). + /// When enabled, every message passing through the Qt message handler is + /// recorded with its category, type and raw text. + static void setCaptureEnabled(bool enabled); + + /// Discard all previously captured messages. + static void clearCapturedMessages(); + + /// Return captured messages, optionally filtered to a single category. + /// Pass an empty string to retrieve all messages. + [[nodiscard]] static QList capturedMessages(const QString &category = {}); + + /// Return true if at least one message of the given @a type was captured + /// for @a category. + [[nodiscard]] static bool hasCapturedMessage(const QString &category, QtMsgType type); + + /// Convenience: captured warning in @a category? + [[nodiscard]] static bool hasCapturedWarning(const QString &category); + + /// Convenience: captured critical in @a category? + [[nodiscard]] static bool hasCapturedCritical(const QString &category); + + /// Return true if any uncategorized message was captured (e.g. raw qDebug/qWarning). + [[nodiscard]] static bool hasCapturedUncategorizedMessage(); + + /// Record a message into the capture buffer (if capture is enabled). + /// Intended for external handler wrappers (e.g. the unit-test capture handler). + static void captureIfEnabled(QtMsgType type, const QMessageLogContext &context, const QString &msg); + signals: /// Emitted when a log message is enqueued void emitLog(const QString &message); From 6806487ae8631a9209946251b49660e7621f9ec1 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 27 Feb 2026 12:25:13 -0800 Subject: [PATCH 023/119] UnitTest: Install log capture handler during test execution QTest::qExec() replaces the Qt message handler with its own, so QGCLogging::msgHandler is NOT called during test runs. Work around this by installing a thin wrapper (testCaptureHandler) on top of QTest's handler in initTestCase(), restoring it in cleanupTestCase(). - testCaptureHandler calls QGCLogging::captureIfEnabled() then chains to QTest's handler for normal QWARN/QCRITICAL output - init() enables capture and clears previous messages per-test - cleanup() disables capture after each test finishes --- test/UnitTestFramework/UnitTest.cc | 40 +++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/test/UnitTestFramework/UnitTest.cc b/test/UnitTestFramework/UnitTest.cc index b97ebb35157c..96f7820751ad 100644 --- a/test/UnitTestFramework/UnitTest.cc +++ b/test/UnitTestFramework/UnitTest.cc @@ -19,6 +19,7 @@ #include "MultiVehicleManager.h" #include "QGC.h" #include "QGCApplication.h" +#include "QGCLogging.h" #include "QGCLoggingCategory.h" #include "QmlObjectListModel.h" #include "SettingsManager.h" @@ -626,15 +627,45 @@ int UnitTest::run(QStringView singleTest, const QString& outputFile, TestLabels return ret; } +// ============================================================================ +// Test‑time capture handler +// ============================================================================ +// QTest::qExec() replaces the application message handler with its own. +// Our QGCLogging::msgHandler is therefore NOT called during test execution. +// We work around this by installing a thin wrapper ON TOP of QTest's handler +// inside initTestCase() and restoring it in cleanupTestCase(). +// The wrapper captures messages for the test log‑capture API and then chains +// to QTest's handler so QWARN / QCRITICAL output keeps working. + +static QtMessageHandler s_qtestHandler = nullptr; + +static void testCaptureHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + // Capture for unit‑test introspection + QGCLogging::captureIfEnabled(type, context, msg); + + // Chain to QTest's handler for normal test output + if (s_qtestHandler) { + s_qtestHandler(type, context, msg); + } +} + void UnitTest::initTestCase() { // Reset test tracking state at start of each test class _resetTestState(); + + // Install the capture wrapper on top of QTest's handler + s_qtestHandler = qInstallMessageHandler(testCaptureHandler); } void UnitTest::cleanupTestCase() { - // Override in derived classes for one-time teardown + // Restore QTest's handler so qExec teardown stays consistent + if (s_qtestHandler) { + (void) qInstallMessageHandler(s_qtestHandler); + s_qtestHandler = nullptr; + } } void UnitTest::init() @@ -642,6 +673,10 @@ void UnitTest::init() _initCalled = true; _failureContextDumped = false; + // Start capturing log messages for this test (cleared from previous test) + QGCLogging::clearCapturedMessages(); + QGCLogging::setCaptureEnabled(true); + // Force offline vehicle back to defaults AppSettings* const appSettings = SettingsManager::instance()->appSettings(); appSettings->offlineEditingFirmwareClass()->setRawValue( @@ -655,6 +690,9 @@ void UnitTest::cleanup() _cleanupCalled = true; dumpFailureContextIfTestFailed(QStringLiteral("cleanup")); + // Stop capturing log messages after the test finishes + QGCLogging::setCaptureEnabled(false); + _cleanupTempFiles(); // Process any lingering events to prevent cross-test contamination From 61d5b29625d644b96dfe5902eb800b9074aa3b68 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 27 Feb 2026 12:25:24 -0800 Subject: [PATCH 024/119] QGCStateMachine: Promote programming errors to critical Elevate qCWarning to qCCritical for conditions that indicate programming or configuration errors in the state machine module: - start() called on an already-running machine - AsyncFunctionState with no completion connection and no timeout - SubMachineState with null or broken factory - SendMavlinkCommandState used before configuration - resetToState() with null or foreign state These are internal errors, not runtime conditions. Promoting them ensures the test log capture infrastructure catches them as failures. --- src/Utilities/StateMachine/QGCStateMachine.cc | 6 +++--- src/Utilities/StateMachine/States/AsyncFunctionState.cc | 2 +- .../StateMachine/States/SendMavlinkCommandState.cc | 2 +- src/Utilities/StateMachine/States/SubMachineState.cc | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Utilities/StateMachine/QGCStateMachine.cc b/src/Utilities/StateMachine/QGCStateMachine.cc index 6f764dc735db..88215eb91314 100644 --- a/src/Utilities/StateMachine/QGCStateMachine.cc +++ b/src/Utilities/StateMachine/QGCStateMachine.cc @@ -63,7 +63,7 @@ QString QGCStateMachine::currentStateName() const void QGCStateMachine::start() { if (isRunning()) { - qCWarning(QGCStateMachineLog) << objectName() << "start() called but already running - check signal connections"; + qCCritical(QGCStateMachineLog) << objectName() << "start() called but already running - check signal connections"; } QStateMachine::start(); } @@ -318,13 +318,13 @@ void QGCStateMachine::clearError(bool restart) bool QGCStateMachine::resetToState(QAbstractState* state) { if (!state) { - qCWarning(QGCStateMachineLog) << objectName() << "resetToState: null state"; + qCCritical(QGCStateMachineLog) << objectName() << "resetToState: null state"; return false; } // Verify the state belongs to this machine if (state->parentState() != this && state->parent() != this) { - qCWarning(QGCStateMachineLog) << objectName() << "resetToState: state does not belong to this machine"; + qCCritical(QGCStateMachineLog) << objectName() << "resetToState: state does not belong to this machine"; return false; } diff --git a/src/Utilities/StateMachine/States/AsyncFunctionState.cc b/src/Utilities/StateMachine/States/AsyncFunctionState.cc index deb5a30c275f..16d792490941 100644 --- a/src/Utilities/StateMachine/States/AsyncFunctionState.cc +++ b/src/Utilities/StateMachine/States/AsyncFunctionState.cc @@ -27,7 +27,7 @@ void AsyncFunctionState::onWaitEntered() } if (!_completionConnection && timeoutMsecs() == 0) { - qCWarning(QGCStateMachineLog) << stateName() + qCCritical(QGCStateMachineLog) << stateName() << "has no completion connection and no timeout - state may hang indefinitely"; } } diff --git a/src/Utilities/StateMachine/States/SendMavlinkCommandState.cc b/src/Utilities/StateMachine/States/SendMavlinkCommandState.cc index 4a020e6e167e..6c0d5ec46675 100644 --- a/src/Utilities/StateMachine/States/SendMavlinkCommandState.cc +++ b/src/Utilities/StateMachine/States/SendMavlinkCommandState.cc @@ -42,7 +42,7 @@ void SendMavlinkCommandState::disconnectWaitSignal() void SendMavlinkCommandState::onWaitEntered() { if (!_configured) { - qCWarning(QGCStateMachineLog) << "SendMavlinkCommandState not configured"; + qCCritical(QGCStateMachineLog) << "SendMavlinkCommandState not configured"; waitFailed(); return; } diff --git a/src/Utilities/StateMachine/States/SubMachineState.cc b/src/Utilities/StateMachine/States/SubMachineState.cc index ce347e74821b..5277a65600c5 100644 --- a/src/Utilities/StateMachine/States/SubMachineState.cc +++ b/src/Utilities/StateMachine/States/SubMachineState.cc @@ -13,14 +13,14 @@ void SubMachineState::onEntry(QEvent* event) QGCState::onEntry(event); if (!_factory) { - qCWarning(QGCStateMachineLog) << stateName() << "no machine factory provided"; + qCCritical(QGCStateMachineLog) << stateName() << "no machine factory provided"; emit error(); return; } _childMachine = _factory(this); if (!_childMachine) { - qCWarning(QGCStateMachineLog) << stateName() << "factory returned null machine"; + qCCritical(QGCStateMachineLog) << stateName() << "factory returned null machine"; emit error(); return; } From b540d54ef82269235ca84a0c6d50101f6aa581f7 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 27 Feb 2026 12:26:24 -0800 Subject: [PATCH 025/119] InitialConnectTest: Add timeout fallthrough tests and log validation Add data-driven _stateTimeoutFallsThrough test that exercises timeout fallthrough for each InitialConnectStateMachine state individually: - StandardModes: block AVAILABLE_MODES - CompInfo: block COMPONENT_METADATA - Parameters: no param response - Mission: block mission protocol from start - GeoFence: block mission protocol after mission loads Each row verifies the state machine completes despite the timeout, and checks parametersReady / initialPlanRequestComplete expectations. Add cleanup() override that scans captured log messages and fails the test if any uncategorized messages (raw qDebug/qWarning without a category) or critical-level messages were logged during the test run. This catches production code quality issues like missing logging categories and unexpected error conditions. Note: The CompInfo row currently exposes a real bug where ComponentInformationManager::requestAllComponentInformation() calls start() on an already-running state machine. That bug should be fixed separately in production code. --- test/Vehicle/InitialConnectTest.cc | 164 +++++++++++++++++++++++++++++ test/Vehicle/InitialConnectTest.h | 3 + 2 files changed, 167 insertions(+) diff --git a/test/Vehicle/InitialConnectTest.cc b/test/Vehicle/InitialConnectTest.cc index c038cb9ab2b8..d554db710394 100644 --- a/test/Vehicle/InitialConnectTest.cc +++ b/test/Vehicle/InitialConnectTest.cc @@ -10,8 +10,10 @@ #include "MAVLinkProtocol.h" #include "MultiVehicleManager.h" #include "MockConfiguration.h" +#include "MockLink.h" #include "MockLinkMissionItemHandler.h" #include "MissionManager.h" +#include "QGCLogging.h" #include "ParameterManager.h" #include "RallyPointManager.h" #include "StandardModes.h" @@ -26,6 +28,43 @@ void InitialConnectTest::init() VehicleTestManualConnect::init(); } +void InitialConnectTest::cleanup() +{ + // Snapshot log violations before parent cleanup disables capture + QString uncategorizedDetails; + QString criticalDetails; + + const auto allMsgs = QGCLogging::capturedMessages(); + for (const auto &m : allMsgs) { + if (m.category.isEmpty() || m.category == QStringLiteral("default")) { + const char *level = (m.type == QtDebugMsg) ? "debug" + : (m.type == QtWarningMsg) ? "warning" + : (m.type == QtInfoMsg) ? "info" + : "other"; + uncategorizedDetails += QStringLiteral(" [%1] %2\n").arg(QLatin1String(level), m.message); + } + if (m.type == QtCriticalMsg) { + criticalDetails += QStringLiteral(" [%1] %2\n").arg(m.category, m.message); + } + } + + // Always perform parent cleanup for proper teardown + VehicleTestManualConnect::cleanup(); + + // Report failures after teardown so test infrastructure stays consistent + if (!uncategorizedDetails.isEmpty() || !criticalDetails.isEmpty()) { + QString msg; + if (!uncategorizedDetails.isEmpty()) { + msg += QStringLiteral("Uncategorized log messages (use qCDebug/qCWarning with a category):\n%1") + .arg(uncategorizedDetails); + } + if (!criticalDetails.isEmpty()) { + msg += QStringLiteral("Critical log messages:\n%1").arg(criticalDetails); + } + QFAIL(qPrintable(msg)); + } +} + void InitialConnectTest::_performTestCases_data() { QTest::addColumn("failureMode"); @@ -247,6 +286,131 @@ void InitialConnectTest::_rallyTimeoutPathDoesNotLeakCompletionHandler() _disconnectMockLink(); } +void InitialConnectTest::_stateTimeoutFallsThrough_data() +{ + QTest::addColumn>("blockedMessageIds"); + QTest::addColumn("configFailureMode"); + QTest::addColumn("blockMissionProtocolImmediately"); + QTest::addColumn("blockMissionProtocolAfterMissionLoad"); + QTest::addColumn("timeoutOverrideStates"); + QTest::addColumn("expectParametersReady"); + QTest::addColumn("expectPlanRequestComplete"); + + // Timeout matrix: + // +----------------+-------------------+------------------+---------+--------+ + // | State | Failure Injection | Timeout States | Params? | Plans? | + // +----------------+-------------------+------------------+---------+--------+ + // | StandardModes | Block AVAIL_MODES | StdModes | Yes | Yes | + // | CompInfo | Block COMP_META | CompInfo | Yes | Yes | + // | Parameters | No param response | Parameters | No | Yes | + // | Mission | Block mission req | Msn+Fence+Rally | Yes | No | + // | GeoFence | Block after msn | Fence+Rally | Yes | No | + // +----------------+-------------------+------------------+---------+--------+ + + QTest::addRow("StandardModes") + << QList{MAVLINK_MSG_ID_AVAILABLE_MODES} + << static_cast(MockConfiguration::FailNone) + << false << false + << QStringList{QStringLiteral("RequestStandardModes")} + << true << true; + + QTest::addRow("CompInfo") + << QList{MAVLINK_MSG_ID_COMPONENT_METADATA} + << static_cast(MockConfiguration::FailNone) + << false << false + << QStringList{QStringLiteral("RequestCompInfo")} + << true << true; + + QTest::addRow("Parameters") + << QList{} + << static_cast(MockConfiguration::FailParamNoResponseToRequestList) + << false << false + << QStringList{QStringLiteral("RequestParameters")} + << false << true; + + QTest::addRow("Mission") + << QList{} + << static_cast(MockConfiguration::FailNone) + << true << false + << QStringList{QStringLiteral("RequestMission"), + QStringLiteral("RequestGeoFence"), + QStringLiteral("RequestRallyPoints")} + << true << false; + + QTest::addRow("GeoFence") + << QList{} + << static_cast(MockConfiguration::FailNone) + << false << true + << QStringList{QStringLiteral("RequestGeoFence"), + QStringLiteral("RequestRallyPoints")} + << true << false; +} + +void InitialConnectTest::_stateTimeoutFallsThrough() +{ + QFETCH(QList, blockedMessageIds); + QFETCH(int, configFailureMode); + QFETCH(bool, blockMissionProtocolImmediately); + QFETCH(bool, blockMissionProtocolAfterMissionLoad); + QFETCH(QStringList, timeoutOverrideStates); + QFETCH(bool, expectParametersReady); + QFETCH(bool, expectPlanRequestComplete); + + LinkManager::instance()->setConnectionsAllowed(); + + auto* mvm = MultiVehicleManager::instance(); + QSignalSpy activeVehicleSpy{mvm, &MultiVehicleManager::activeVehicleChanged}; + auto* mockConfig = new MockConfiguration(QStringLiteral("TimeoutFallthroughMock")); + mockConfig->setFirmwareType(MAV_AUTOPILOT_PX4); + mockConfig->setVehicleType(MAV_TYPE_QUADROTOR); + if (configFailureMode != static_cast(MockConfiguration::FailNone)) { + mockConfig->setFailureMode(static_cast(configFailureMode)); + } + mockConfig->setDynamic(true); + + SharedLinkConfigurationPtr linkConfig = LinkManager::instance()->addConfiguration(mockConfig); + QVERIFY(LinkManager::instance()->createConnectedLink(linkConfig)); + + _mockLink = qobject_cast(linkConfig->link()); + QVERIFY(_mockLink); + + for (const uint32_t messageId : blockedMessageIds) { + _mockLink->setRequestMessageNoResponse(messageId); + } + + if (blockMissionProtocolImmediately) { + _mockLink->setMissionItemFailureMode( + MockLinkMissionItemHandler::FailReadRequestListNoResponse, MAV_MISSION_ACCEPTED); + } + + QVERIFY(activeVehicleSpy.wait(TestTimeout::longMs())); + _vehicle = mvm->activeVehicle(); + QVERIFY(_vehicle); + + auto* initialConnectStateMachine = _vehicle->findChild(); + QVERIFY(initialConnectStateMachine); + + for (const QString& stateName : timeoutOverrideStates) { + initialConnectStateMachine->setTimeoutOverride(stateName, 100); + } + + if (blockMissionProtocolAfterMissionLoad) { + auto* missionManager = _vehicle->findChild(); + QVERIFY(missionManager); + connect(missionManager, &MissionManager::newMissionItemsAvailable, this, [this]() { + _mockLink->setMissionItemFailureMode( + MockLinkMissionItemHandler::FailReadRequestListNoResponse, MAV_MISSION_ACCEPTED); + }); + } + + QSignalSpy initialConnectCompleteSpy{_vehicle, &Vehicle::initialConnectComplete}; + QVERIFY(initialConnectCompleteSpy.wait(60000) || _vehicle->isInitialConnectComplete()); + QCOMPARE(_vehicle->parameterManager()->parametersReady(), expectParametersReady); + QCOMPARE(_vehicle->initialPlanRequestComplete(), expectPlanRequestComplete); + + _disconnectMockLink(); +} + void InitialConnectTest::_stateRunMatrix_data() { QTest::addColumn("highLatency"); diff --git a/test/Vehicle/InitialConnectTest.h b/test/Vehicle/InitialConnectTest.h index 9bbd17d159ed..da7ce809cab4 100644 --- a/test/Vehicle/InitialConnectTest.h +++ b/test/Vehicle/InitialConnectTest.h @@ -8,6 +8,7 @@ class InitialConnectTest : public VehicleTestManualConnect private slots: void init() override; + void cleanup() override; void _performTestCases_data(); void _performTestCases(); void _boardVendorProductId(); @@ -16,6 +17,8 @@ private slots: void _genericAutopilotVersionFailureSkipsUnsupportedPlanTypes(); void _multipleReconnects(); void _rallyTimeoutPathDoesNotLeakCompletionHandler(); + void _stateTimeoutFallsThrough_data(); + void _stateTimeoutFallsThrough(); void _stateRunMatrix_data(); void _stateRunMatrix(); }; From 34564bbda7db3aa37463f2fd18c5f5f414232618 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 27 Feb 2026 14:47:40 -0800 Subject: [PATCH 026/119] Fix ComponentInformationManager double-start on timeout retry When InitialConnectStateMachine's CompInfo state times out, the retry callback re-invokes requestAllComponentInformation() while the CIM state machine is still running, triggering a critical 'start() called but already running' error. Guard requestAllComponentInformation() with isRunning() so the retry path updates the completion callback without restarting. Also add setOnExit cleanup for the progress signal connection (consistent with Parameters, Mission, GeoFence, and Rally states). --- .../ComponentInformationManager.cc | 8 +++++++- src/Vehicle/InitialConnectStateMachine.cc | 10 +++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Vehicle/ComponentInformation/ComponentInformationManager.cc b/src/Vehicle/ComponentInformation/ComponentInformationManager.cc index 8546ffdeeb9c..cd5a512d8832 100644 --- a/src/Vehicle/ComponentInformation/ComponentInformationManager.cc +++ b/src/Vehicle/ComponentInformation/ComponentInformationManager.cc @@ -179,7 +179,13 @@ void ComponentInformationManager::requestAllComponentInformation(RequestAllCompl _requestAllCompleteFn = requestAllCompletFn; _requestAllCompleteFnData = requestAllCompleteFnData; - start(); + // Guard against double-start: when InitialConnectStateMachine's CompInfo + // state times out, the retry callback re-invokes this method while the CIM + // state machine is still running. Only start if not already in progress; + // the updated callback pointers above are sufficient for the retry path. + if (!isRunning()) { + start(); + } emit progressUpdate(progress()); } diff --git a/src/Vehicle/InitialConnectStateMachine.cc b/src/Vehicle/InitialConnectStateMachine.cc index db4dd05b8b10..f2b732180428 100644 --- a/src/Vehicle/InitialConnectStateMachine.cc +++ b/src/Vehicle/InitialConnectStateMachine.cc @@ -331,18 +331,22 @@ void InitialConnectStateMachine::_requestStandardModes(AsyncFunctionState* state vehicle()->_standardModes->request(); } -void InitialConnectStateMachine::_requestCompInfo(AsyncFunctionState* /*state*/) +void InitialConnectStateMachine::_requestCompInfo(AsyncFunctionState* state) { qCDebug(InitialConnectStateMachineLog) << "_stateRequestCompInfo"; connect(vehicle()->_componentInformationManager, &ComponentInformationManager::progressUpdate, this, &InitialConnectStateMachine::_onSubProgressUpdate, Qt::UniqueConnection); + // Ensure progress tracking is always cleaned up, including timeout/skip paths. + state->setOnExit([this]() { + disconnect(vehicle()->_componentInformationManager, &ComponentInformationManager::progressUpdate, + this, &InitialConnectStateMachine::_onSubProgressUpdate); + }); + vehicle()->_componentInformationManager->requestAllComponentInformation( [](void* requestAllCompleteFnData) { auto* self = static_cast(requestAllCompleteFnData); - disconnect(self->vehicle()->_componentInformationManager, &ComponentInformationManager::progressUpdate, - self, &InitialConnectStateMachine::_onSubProgressUpdate); if (self->_stateCompInfo) { self->_stateCompInfo->complete(); } From c5887c870a7d406106f957c87b17a7fb88a4f0d6 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 4 Mar 2026 13:28:00 -0800 Subject: [PATCH 027/119] Address PR review: fix MockLink thread-safety, spy wait ordering, and log-capture race - Guard _requestMessageNoResponseIds with QMutex in both setter and reader - Check isInitialConnectComplete() before spy.wait() to avoid 60s stall - Disable log capture before snapshotting in cleanup() for deterministic results --- src/Comms/MockLink/MockLink.cc | 9 ++++++--- src/Comms/MockLink/MockLink.h | 2 ++ test/Vehicle/InitialConnectTest.cc | 8 ++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Comms/MockLink/MockLink.cc b/src/Comms/MockLink/MockLink.cc index 59b0e8f5a378..90b352fd6368 100644 --- a/src/Comms/MockLink/MockLink.cc +++ b/src/Comms/MockLink/MockLink.cc @@ -1985,9 +1985,12 @@ void MockLink::_handleRequestMessage(const mavlink_command_long_t &request, bool const uint32_t requestedMessageId = static_cast(request.param1); // Per-message-ID no-response injection: silently drop the request (no ACK, no message) - if (_requestMessageNoResponseIds.contains(requestedMessageId)) { - noAck = true; - return; + { + QMutexLocker locker(&_requestMessageNoResponseMutex); + if (_requestMessageNoResponseIds.contains(requestedMessageId)) { + noAck = true; + return; + } } switch (static_cast(request.param1)) { diff --git a/src/Comms/MockLink/MockLink.h b/src/Comms/MockLink/MockLink.h index 5643dffe7799..e42d383bf06a 100644 --- a/src/Comms/MockLink/MockLink.h +++ b/src/Comms/MockLink/MockLink.h @@ -99,6 +99,7 @@ class MockLink : public LinkInterface /// Block or unblock REQUEST_MESSAGE responses for a specific message ID. /// When blocked, MockLink silently drops the request (no ACK, no message). void setRequestMessageNoResponse(uint32_t messageId, bool noResponse = true) { + QMutexLocker locker(&_requestMessageNoResponseMutex); if (noResponse) { _requestMessageNoResponseIds.insert(messageId); } else { @@ -319,6 +320,7 @@ private slots: QMutex _logDownloadMutex; RequestMessageFailureMode_t _requestMessageFailureMode = FailRequestMessageNone; + mutable QMutex _requestMessageNoResponseMutex; QSet _requestMessageNoResponseIds; ParamSetFailureMode_t _paramSetFailureMode = FailParamSetNone; bool _paramSetFailureFirstAttemptPending = false; diff --git a/test/Vehicle/InitialConnectTest.cc b/test/Vehicle/InitialConnectTest.cc index d554db710394..878ab174a87e 100644 --- a/test/Vehicle/InitialConnectTest.cc +++ b/test/Vehicle/InitialConnectTest.cc @@ -30,7 +30,9 @@ void InitialConnectTest::init() void InitialConnectTest::cleanup() { - // Snapshot log violations before parent cleanup disables capture + // Disable capture first so no new messages arrive while we analyze + QGCLogging::setCaptureEnabled(false); + QString uncategorizedDetails; QString criticalDetails; @@ -404,7 +406,9 @@ void InitialConnectTest::_stateTimeoutFallsThrough() } QSignalSpy initialConnectCompleteSpy{_vehicle, &Vehicle::initialConnectComplete}; - QVERIFY(initialConnectCompleteSpy.wait(60000) || _vehicle->isInitialConnectComplete()); + if (!_vehicle->isInitialConnectComplete()) { + QVERIFY(initialConnectCompleteSpy.wait(60000)); + } QCOMPARE(_vehicle->parameterManager()->parametersReady(), expectParametersReady); QCOMPARE(_vehicle->initialPlanRequestComplete(), expectPlanRequestComplete); From 70dc74ecde0da88beec5f57b95ab15b9caf3d08a Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 5 Mar 2026 08:35:55 -0800 Subject: [PATCH 028/119] QGCTabBar: filter non-button children from ButtonGroup Filter RowLayout children to only include items with a 'checkable' property (i.e. actual tab buttons) before assigning them to ButtonGroup. This fixes ButtonGroup errors when Repeater or other non-button items are among the RowLayout children. - Build filtered _buttons array and assign to ButtonGroup - Counter-rotate currentIndex logic to use _buttons instead of children - Guard onCheckedButtonChanged against binding loops - Re-select on children/visibility changes --- src/QmlControls/QGCTabBar.qml | 67 +++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/src/QmlControls/QGCTabBar.qml b/src/QmlControls/QGCTabBar.qml index 8c52b2ed34c6..2fc7d98f0bd4 100644 --- a/src/QmlControls/QGCTabBar.qml +++ b/src/QmlControls/QGCTabBar.qml @@ -15,43 +15,66 @@ RowLayout { spacing: 0 property bool _preventCurrentIndexBindingLoop: false + property var _buttons: [] + + function _updateButtons() { + let btns = [] + for (var i = 0; i < control.children.length; i++) { + if (control.children[i].hasOwnProperty("checkable")) { + btns.push(control.children[i]) + } + } + _buttons = btns + buttonGroup.buttons = _buttons + _selectCurrentIndexButton() + } function _selectCurrentIndexButton() { - if (control.children.length > 0) { - _preventCurrentIndexBindingLoop = true - if (control.currentIndex == -1) { - // No tab selected, select none - for (var i = 0; i < control.children.length; i++) { - control.children[i].checked = false - } + if (_buttons.length === 0) return + + _preventCurrentIndexBindingLoop = true + + if (control.currentIndex === -1) { + for (var i = 0; i < _buttons.length; i++) { + _buttons[i].checked = false + } + } else { + let index = Math.min(Math.max(control.currentIndex, 0), _buttons.length - 1) + if (_buttons[index].visible) { + _buttons[index].checked = true } else { - let filteredCurrentIndex = Math.min(Math.max(control.currentIndex, 0), control.children.length - 1) - if (control.children[filteredCurrentIndex].visible) { - control.children[filteredCurrentIndex].checked = true - } else { - // We select the first visible tab if the current index is not visible - for (var j = 0; j < control.children.length; j++) { - if (control.children[j].visible) { - control.children[j].checked = true - break - } + // Select the first visible tab if the current index is not visible + index = -1 + for (var j = 0; j < _buttons.length; j++) { + if (_buttons[j].visible) { + _buttons[j].checked = true + index = j + break } } } - _preventCurrentIndexBindingLoop = false + // Sync currentIndex to the actually-selected button so it never remains stale + if (control.currentIndex !== index) { + control.currentIndex = index + } } + + _preventCurrentIndexBindingLoop = false } - Component.onCompleted: _selectCurrentIndexButton() + Component.onCompleted: _updateButtons() + onChildrenChanged: _updateButtons() onCurrentIndexChanged: _selectCurrentIndexButton() onVisibleChildrenChanged: _selectCurrentIndexButton() ButtonGroup { - buttons: control.children + id: buttonGroup onCheckedButtonChanged: { - for (var i = 0; i < control.children.length; i++) { - if (control.children[i] === checkedButton) { + if (control._preventCurrentIndexBindingLoop) return + + for (var i = 0; i < control._buttons.length; i++) { + if (control._buttons[i] === checkedButton) { control.currentIndex = i break } From 427ecaeaaff3928f13ced0d91f77605719df33b8 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 5 Mar 2026 18:16:52 -0800 Subject: [PATCH 029/119] PlanView: Replace flat mission item list with collapsible TreeView Replace the plan view right panel's flat QmlObjectListModel-based editor list with a QML TreeView backed by a new QmlObjectTreeModel. Mission Items, GeoFence, and Rally Points are now presented as collapsible groups with exclusive expand behavior (only one group open at a time). Clicking an already-expanded group collapses it. Structural changes: - Extract ObjectItemModelBase from ObjectListModelBase to share dirty tracking, nested beginResetModel/endResetModel guards, count-changed signal suppression, and role definitions between list and tree models. - Add QmlObjectTreeModel (QAbstractItemModel for trees) with cached node count, nodeType role, subtree insert/remove, and full dirty propagation from child QObjects. - Add MissionController::visualItemsTree() which maintains a QmlObjectTreeModel shadow of visualItems with three top-level groups (missionGroup, fenceGroup, rallyGroup) and syncs on insert/remove. - Rally point signal wiring lives in _initAllVisualItems so connections are refreshed on each plan reinit. - insertRows/removeRows overrides log warnings instead of silently failing (prevents misuse from QML or proxy models). QML changes: - Add MissionItemTreeView.qml: TreeView delegate dispatches to MissionItemEditor, GeoFenceEditor, RallyPointEditorHeader, and RallyPointItemEditor based on nodeType. Group headers show expand/collapse indicators with highlight for the active group. - Simplify PlanViewRightPanel.qml to host MissionItemTreeView directly instead of managing three separate editor views and a tab-like switcher. - After collapse/expand, the view scrolls so the group header is visible (prevents blank view when expanding GeoFence/Rally after scrolling past many waypoints). --- src/MissionManager/MissionController.cc | 163 +++++++- src/MissionManager/MissionController.h | 25 +- src/QmlControls/CMakeLists.txt | 5 + src/QmlControls/MissionItemEditor.qml | 2 +- src/QmlControls/MissionItemTreeView.qml | 254 ++++++++++++ src/QmlControls/ObjectItemModelBase.cc | 74 ++++ src/QmlControls/ObjectItemModelBase.h | 60 +++ src/QmlControls/ObjectListModelBase.cc | 63 +-- src/QmlControls/ObjectListModelBase.h | 50 +-- src/QmlControls/PlanViewRightPanel.qml | 127 +----- src/QmlControls/QmlObjectTreeModel.cc | 513 ++++++++++++++++++++++++ src/QmlControls/QmlObjectTreeModel.h | 120 ++++++ 12 files changed, 1247 insertions(+), 209 deletions(-) create mode 100644 src/QmlControls/MissionItemTreeView.qml create mode 100644 src/QmlControls/ObjectItemModelBase.cc create mode 100644 src/QmlControls/ObjectItemModelBase.h create mode 100644 src/QmlControls/QmlObjectTreeModel.cc create mode 100644 src/QmlControls/QmlObjectTreeModel.h diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 79f39b271d8f..c9d856e506bc 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -123,6 +123,10 @@ void MissionController::_init(void) { // We start with an empty mission _addMissionSettings(_visualItems); + + // Set up the tree model structure once (groups + static children) + _setupTreeModel(); + _initAllVisualItems(); } @@ -533,6 +537,11 @@ void MissionController::_insertComplexMissionItemWorker(const QGeoCoordinate& ma _firstItemAdded(); } +int MissionController::visualItemIndexForObject(QObject* object) const +{ + return _visualItems ? _visualItems->indexOf(object) : -1; +} + void MissionController::removeVisualItem(int viIndex) { if (viIndex <= 0 || viIndex >= _visualItems->count()) { @@ -1804,6 +1813,118 @@ void MissionController::_recalcChildItems(void) } } +void MissionController::_setupTreeModel(void) +{ + // Tree starts empty — no need for beginReset/endReset. + // We use incremental inserts so QPersistentModelIndex stays valid. + _visualItemsTree.clear(); + + // ── Mission Items group ── + _missionItemsGroupNode.setObjectName(tr("Mission Items")); + _missionGroupIndex = QPersistentModelIndex( + _visualItemsTree.appendItem(&_missionItemsGroupNode, QModelIndex(), QStringLiteral("missionGroup"))); + + // ── GeoFence group ── + _fenceGroupNode.setObjectName(tr("GeoFence")); + _fenceGroupIndex = QPersistentModelIndex( + _visualItemsTree.appendItem(&_fenceGroupNode, QModelIndex(), QStringLiteral("fenceGroup"))); + + // Single marker child — delegate loads the full GeoFenceEditor + _fenceEditorMarker.setObjectName(QStringLiteral("fenceEditor")); + _visualItemsTree.appendItem(&_fenceEditorMarker, _fenceGroupIndex, QStringLiteral("fenceEditor")); + + // ── Rally Points group ── + _rallyGroupNode.setObjectName(tr("Rally Points")); + _rallyGroupIndex = QPersistentModelIndex( + _visualItemsTree.appendItem(&_rallyGroupNode, QModelIndex(), QStringLiteral("rallyGroup"))); + + // Marker child for the rally header / instructions + _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader")); + _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader")); +} + +//----------------------------------------------------------------------------- +// Incremental tree model sync — Mission Items +//----------------------------------------------------------------------------- + +void MissionController::_onMissionItemsInserted(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + for (int i = first; i <= last; i++) { + auto* item = _visualItems->value(i); + if (item) { + _visualItemsTree.insertItem(i, item, _missionGroupIndex, QStringLiteral("missionItem")); + } + } +} + +void MissionController::_onMissionItemsAboutToBeRemoved(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + // Remove in reverse order so indexes stay valid + for (int i = last; i >= first; i--) { + _visualItemsTree.removeAt(_missionGroupIndex, i); + } +} + +void MissionController::_onMissionItemsReset(void) +{ + // Clear mission group children and repopulate from _visualItems + _visualItemsTree.removeChildren(_missionGroupIndex); + + if (_visualItems) { + for (int i = 0; i < _visualItems->count(); i++) { + auto* item = _visualItems->value(i); + if (item) { + _visualItemsTree.appendItem(item, _missionGroupIndex, QStringLiteral("missionItem")); + } + } + } +} + +//----------------------------------------------------------------------------- +// Incremental tree model sync — Rally Points +//----------------------------------------------------------------------------- + +void MissionController::_onRallyPointsInserted(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + auto* rallyController = _masterController->rallyPointController(); + if (!rallyController) return; + + auto* pts = rallyController->points(); + // Rally children: index 0 is the rallyHeader marker, rally items start at index 1 + for (int i = first; i <= last; i++) { + _visualItemsTree.insertItem(i + 1, (*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem")); + } +} + +void MissionController::_onRallyPointsAboutToBeRemoved(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + // Rally children: index 0 is header marker, rally items start at index 1 + for (int i = last; i >= first; i--) { + _visualItemsTree.removeAt(_rallyGroupIndex, i + 1); + } +} + +void MissionController::_onRallyPointsReset(void) +{ + // Remove all rally children except the header marker (index 0) + while (_visualItemsTree.rowCount(_rallyGroupIndex) > 1) { + _visualItemsTree.removeAt(_rallyGroupIndex, _visualItemsTree.rowCount(_rallyGroupIndex) - 1); + } + + // Repopulate + auto* rallyController = _masterController->rallyPointController(); + if (rallyController) { + auto* pts = rallyController->points(); + for (int i = 0; i < pts->count(); i++) { + _visualItemsTree.appendItem((*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem")); + } + } +} + void MissionController::_setPlannedHomePositionFromFirstCoordinate(const QGeoCoordinate& clickCoordinate) { bool foundFirstCoordinate = false; @@ -1884,6 +2005,26 @@ void MissionController::_initAllVisualItems(void) connect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged); connect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::_updateContainsItems); + // Connect for incremental tree model sync + connect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_onMissionItemsInserted); + connect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_onMissionItemsAboutToBeRemoved); + connect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_onMissionItemsReset); + + // Populate tree's mission group from current _visualItems + _onMissionItemsReset(); + + // Connect rally controller for incremental tree model sync + auto* rallyController = _masterController->rallyPointController(); + if (rallyController) { + auto* pts = rallyController->points(); + connect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_onRallyPointsInserted); + connect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_onRallyPointsAboutToBeRemoved); + connect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_onRallyPointsReset); + + // Populate any existing rally points + _onRallyPointsReset(); + } + emit visualItemsChanged(); emit containsItemsChanged(); emit plannedHomePositionChanged(plannedHomePosition()); @@ -1904,8 +2045,22 @@ void MissionController::_deinitAllVisualItems(void) _deinitVisualItem(qobject_cast(_visualItems->get(i))); } - disconnect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::dirtyChanged); + disconnect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged); disconnect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::_updateContainsItems); + + // Disconnect incremental tree model sync + disconnect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_onMissionItemsInserted); + disconnect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_onMissionItemsAboutToBeRemoved); + disconnect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_onMissionItemsReset); + + // Disconnect rally controller tree model sync + auto* rallyController = _masterController->rallyPointController(); + if (rallyController) { + auto* pts = rallyController->points(); + disconnect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_onRallyPointsInserted); + disconnect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_onRallyPointsAboutToBeRemoved); + disconnect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_onRallyPointsReset); + } } void MissionController::_initVisualItem(VisualMissionItem* visualItem) @@ -1946,8 +2101,10 @@ void MissionController::_initVisualItem(VisualMissionItem* visualItem) void MissionController::_deinitVisualItem(VisualMissionItem* visualItem) { - // Disconnect all signals - disconnect(visualItem, nullptr, nullptr, nullptr); + // Disconnect only signals connected to this MissionController, not all receivers. + // A full wildcard disconnect(obj, 0, 0, 0) tears out internal destroyed-signal + // connections that Qt (and the tree/list models) rely on for cleanup. + disconnect(visualItem, nullptr, this, nullptr); } void MissionController::_itemCommandChanged(void) diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 8a45f38dbcf9..07bbfa61867a 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -3,10 +3,12 @@ #include #include #include +#include #include #include "PlanElementController.h" #include "QmlObjectListModel.h" +#include "QmlObjectTreeModel.h" #include "QGCGeoBoundingCube.h" #include "QGroundControlQmlGlobal.h" #include "QGCMAVLink.h" @@ -71,6 +73,7 @@ class MissionController : public PlanElementController } MissionFlightStatus_t; Q_PROPERTY(QmlObjectListModel* visualItems READ visualItems NOTIFY visualItemsChanged) + Q_PROPERTY(QmlObjectTreeModel* visualItemsTree READ visualItemsTree CONSTANT) ///< Tree-structured view of visualItems for TreeView Q_PROPERTY(QmlObjectListModel* simpleFlightPathSegments READ simpleFlightPathSegments CONSTANT) ///< Used by Plan view only for interactive editing Q_PROPERTY(QmlObjectListModel* directionArrows READ directionArrows CONSTANT) Q_PROPERTY(QStringList complexMissionItemNames READ complexMissionItemNames NOTIFY complexMissionItemNamesChanged) @@ -115,6 +118,9 @@ class MissionController : public PlanElementController Q_INVOKABLE void removeVisualItem(int viIndex); + /// Returns the visual item index for the given VisualMissionItem object, or -1 if not found + Q_INVOKABLE int visualItemIndexForObject(QObject* object) const; + /// Add a new simple mission item to the list /// @param coordinate: Coordinate for item /// @param visualItemIndex: index to insert at, -1 for end of list @@ -220,6 +226,7 @@ class MissionController : public PlanElementController // Property accessors QmlObjectListModel* visualItems (void) { return _visualItems; } + QmlObjectTreeModel* visualItemsTree (void) { return &_visualItemsTree; } QmlObjectListModel* simpleFlightPathSegments (void) { return &_simpleFlightPathSegments; } QmlObjectListModel* directionArrows (void) { return &_directionArrows; } QStringList complexMissionItemNames (void) const; @@ -320,9 +327,16 @@ private slots: void _recalcAll (void); void _managerVehicleChanged (Vehicle* managerVehicle); void _forceRecalcOfAllowedBits (void); - + // Incremental tree model sync slots + void _onMissionItemsInserted (const QModelIndex& parent, int first, int last); + void _onMissionItemsAboutToBeRemoved (const QModelIndex& parent, int first, int last); + void _onMissionItemsReset (void); + void _onRallyPointsInserted (const QModelIndex& parent, int first, int last); + void _onRallyPointsAboutToBeRemoved (const QModelIndex& parent, int first, int last); + void _onRallyPointsReset (void); private: void _init (void); + void _setupTreeModel (void); void _recalcSequence (void); void _recalcChildItems (void); void _recalcAllWithCoordinate (const QGeoCoordinate& coordinate); @@ -370,6 +384,15 @@ private slots: MissionManager* _missionManager = nullptr; int _missionItemCount = 0; QmlObjectListModel* _visualItems = nullptr; + QmlObjectTreeModel _visualItemsTree; + QPersistentModelIndex _missionGroupIndex; ///< Persistent index for "Mission Items" group in tree + QPersistentModelIndex _fenceGroupIndex; ///< Persistent index for "GeoFence" group in tree + QPersistentModelIndex _rallyGroupIndex; ///< Persistent index for "Rally Points" group in tree + QObject _missionItemsGroupNode; ///< Group node for "Mission Items" in tree view + QObject _fenceGroupNode; ///< Group node for "GeoFence" in tree view + QObject _rallyGroupNode; ///< Group node for "Rally Points" in tree view + QObject _fenceEditorMarker; ///< Marker child for GeoFenceEditor delegate + QObject _rallyHeaderMarker; ///< Marker child for RallyPointEditorHeader delegate MissionSettingsItem* _settingsItem = nullptr; PlanViewSettings* _planViewSettings = nullptr; QmlObjectListModel _simpleFlightPathSegments; diff --git a/src/QmlControls/CMakeLists.txt b/src/QmlControls/CMakeLists.txt index e7d53f037699..3f1875bdd474 100644 --- a/src/QmlControls/CMakeLists.txt +++ b/src/QmlControls/CMakeLists.txt @@ -19,6 +19,8 @@ target_sources(${CMAKE_PROJECT_NAME} MavlinkAction.h MavlinkActionManager.cc MavlinkActionManager.h + ObjectItemModelBase.cc + ObjectItemModelBase.h ObjectListModelBase.cc ObjectListModelBase.h ParameterEditorController.cc @@ -49,6 +51,8 @@ target_sources(${CMAKE_PROJECT_NAME} QGroundControlQmlGlobal.h QmlObjectListModel.cc QmlObjectListModel.h + QmlObjectTreeModel.cc + QmlObjectTreeModel.h QmlUnitsConversion.h RCChannelMonitorController.cc RCChannelMonitorController.h @@ -201,6 +205,7 @@ qt_add_qml_module(QGroundControlControlsModule MissionItemEditor.qml MissionItemMapVisual.qml MissionItemStatus.qml + MissionItemTreeView.qml MissionSettingsEditor.qml MissionStats.qml PlanEditToolbar.qml diff --git a/src/QmlControls/MissionItemEditor.qml b/src/QmlControls/MissionItemEditor.qml index 26ca9d0aea3a..f760e9f0cbbc 100644 --- a/src/QmlControls/MissionItemEditor.qml +++ b/src/QmlControls/MissionItemEditor.qml @@ -31,7 +31,7 @@ Rectangle { property var _missionController: _masterController.missionController property bool _currentItem: missionItem.isCurrentItem property color _outerTextColor: _currentItem ? qgcPal.buttonHighlightText : qgcPal.text - property bool _noMissionItemsAdded: ListView.view.model.count === 1 + property bool _noMissionItemsAdded: _missionController.visualItems ? _missionController.visualItems.count <= 1 : true property real _sectionSpacer: ScreenTools.defaultFontPixelWidth / 2 // spacing between section headings property bool _singleComplexItem: _missionController.complexMissionItemNames.length === 1 property bool _readyForSave: missionItem.readyForSaveState === VisualMissionItem.ReadyForSave diff --git a/src/QmlControls/MissionItemTreeView.qml b/src/QmlControls/MissionItemTreeView.qml new file mode 100644 index 000000000000..f77a66149880 --- /dev/null +++ b/src/QmlControls/MissionItemTreeView.qml @@ -0,0 +1,254 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls + +/// Unified plan tree view showing Mission Items, GeoFence, and Rally Points +/// as collapsible sections using a real TreeView with type-discriminating delegates. +TreeView { + id: root + + required property var editorMap + required property var planMasterController + + property var _missionController: planMasterController.missionController + property var _geoFenceController: planMasterController.geoFenceController + property var _rallyPointController: planMasterController.rallyPointController + + model: _missionController.visualItemsTree + clip: true + boundsBehavior: Flickable.StopAtBounds + reuseItems: false + pointerNavigationEnabled: false + selectionBehavior: TableView.SelectionDisabled + rowSpacing: 2 + + // QGCFlickableScrollIndicator expects parent to have indicatorColor (provided by QGCFlickable/QGCListView) + property color indicatorColor: qgcPal.text + + QGCPalette { id: qgcPal; colorGroupEnabled: enabled } + + QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Horizontal } + QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Vertical } + + Component.onCompleted: { + // Expand only Mission Items by default + root.expand(0) + } + + Connections { + target: root._missionController + function onVisualItemsChanged() { + // Mission group always expanded after rebuild (clear / load) + root.collapseRecursively() + root.expand(0) + _editingLayer = _layerMission + } + } + + // Switching editing layer on group expand — exclusive: only one group expanded at a time. + // Clicking an already-expanded group collapses it (toggle behavior). + function _expandExclusive(clickedNodeType) { + // Check if the clicked group is already active + var alreadyActive = (clickedNodeType === "missionGroup" && _editingLayer === _layerMission) || + (clickedNodeType === "fenceGroup" && _editingLayer === _layerFence) || + (clickedNodeType === "rallyGroup" && _editingLayer === _layerRally) + + // Collapse everything + root.collapseRecursively() + + // If the group was already expanded, just leave everything collapsed + if (alreadyActive) { + _editingLayer = -1 + root.forceLayout() + root.positionViewAtRow(0, Qt.AlignTop) + return + } + + // Determine target visual row and editing layer from nodeType + // After full collapse, groups are always at rows 0, 1, 2 + var targetRow = -1 + if (clickedNodeType === "missionGroup") { + targetRow = 0 + _editingLayer = _layerMission + } else if (clickedNodeType === "fenceGroup") { + targetRow = 1 + _editingLayer = _layerFence + } else if (clickedNodeType === "rallyGroup") { + targetRow = 2 + _editingLayer = _layerRally + } + + if (targetRow >= 0) { + root.expand(targetRow) + // After collapse/expand the view may still be scrolled past the new content. + // Force layout then scroll so the expanded group header is at the top. + root.forceLayout() + root.positionViewAtRow(targetRow, Qt.AlignTop) + } + } + + // Coalesces multiple delegate height changes into a single forceLayout() call + Timer { + id: layoutTimer + interval: 0 + running: false + repeat: false + onTriggered: root.forceLayout() + } + + delegate: Item { + id: delegateRoot + + required property TreeView treeView + required property bool isTreeNode + required property bool expanded + required property bool hasChildren + required property int depth + required property int row + required property var model + + readonly property var nodeObject: model.object + readonly property string nodeType: model.nodeType + + implicitWidth: root.width + implicitHeight: loader.item ? loader.item.height : 0 + width: root.width + height: implicitHeight + + onImplicitHeightChanged: layoutTimer.restart() + + Loader { + id: loader + width: parent.width + sourceComponent: { + // Guard: non-group delegates need a valid object. During model + // row removal the role data goes null before the delegate is + // destroyed, which would cause "Cannot read property of null" + // warnings in every downstream editor binding. + switch (delegateRoot.nodeType) { + case "missionGroup": return groupHeaderComponent + case "fenceGroup": return groupHeaderComponent + case "rallyGroup": return groupHeaderComponent + case "missionItem": return delegateRoot.nodeObject ? missionItemComponent : null + case "fenceEditor": return delegateRoot.nodeObject ? fenceEditorComponent : null + case "rallyHeader": return delegateRoot.nodeObject ? rallyHeaderComponent : null + case "rallyItem": return delegateRoot.nodeObject ? rallyItemComponent : null + default: return null + } + } + } + + // ── Group header (Mission Items / GeoFence / Rally Points) ── + Component { + id: groupHeaderComponent + + Rectangle { + width: delegateRoot.width + height: groupHeaderRow.height + ScreenTools.defaultFontPixelHeight * 0.5 + color: _isActiveGroup ? qgcPal.buttonHighlight : qgcPal.windowShade + + property bool _isActiveGroup: { + if (delegateRoot.nodeType === "missionGroup") return _editingLayer === _layerMission + if (delegateRoot.nodeType === "fenceGroup") return _editingLayer === _layerFence + if (delegateRoot.nodeType === "rallyGroup") return _editingLayer === _layerRally + return false + } + + Row { + id: groupHeaderRow + spacing: ScreenTools.defaultFontPixelWidth * 0.5 + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: ScreenTools.defaultFontPixelWidth * 0.5 + + QGCLabel { + text: delegateRoot.expanded ? "\u25BE" : "\u25B8" + color: parent.parent._isActiveGroup ? qgcPal.buttonHighlightText : qgcPal.text + font.pointSize: ScreenTools.defaultFontPointSize + anchors.verticalCenter: parent.verticalCenter + } + + QGCLabel { + text: delegateRoot.nodeObject ? delegateRoot.nodeObject.objectName : "" + font.pointSize: ScreenTools.mediumFontPointSize + font.bold: true + color: parent.parent._isActiveGroup ? qgcPal.buttonHighlightText : qgcPal.text + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + onClicked: root._expandExclusive(delegateRoot.nodeType) + } + } + } + + // ── Mission item delegate ── + Component { + id: missionItemComponent + + MissionItemEditor { + width: delegateRoot.width + map: root.editorMap + masterController: root.planMasterController + missionItem: delegateRoot.nodeObject + readOnly: false + + onClicked: root._missionController.setCurrentPlanViewSeqNum(delegateRoot.nodeObject.sequenceNumber, false) + + onRemove: { + var viIndex = root._missionController.visualItemIndexForObject(delegateRoot.nodeObject) + if (viIndex > 0) { + root._missionController.removeVisualItem(viIndex) + } + } + + onSelectNextNotReadyItem: { + for (var i = 0; i < root._missionController.visualItems.count; i++) { + var vmi = root._missionController.visualItems.get(i) + if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) { + root._missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true) + break + } + } + } + } + } + + // ── GeoFence editor (single child of fence group) ── + Component { + id: fenceEditorComponent + + GeoFenceEditor { + width: delegateRoot.width + myGeoFenceController: root._geoFenceController + flightMap: root.editorMap + } + } + + // ── Rally header / instructions ── + Component { + id: rallyHeaderComponent + + RallyPointEditorHeader { + width: delegateRoot.width + controller: root._rallyPointController + } + } + + // ── Rally point item editor ── + Component { + id: rallyItemComponent + + RallyPointItemEditor { + width: delegateRoot.width + rallyPoint: delegateRoot.nodeObject + controller: root._rallyPointController + } + } + } +} diff --git a/src/QmlControls/ObjectItemModelBase.cc b/src/QmlControls/ObjectItemModelBase.cc new file mode 100644 index 000000000000..ad2a9d47e34d --- /dev/null +++ b/src/QmlControls/ObjectItemModelBase.cc @@ -0,0 +1,74 @@ +/**************************************************************************** + * + * (c) 2009-2024 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +#include "ObjectItemModelBase.h" +#include "QGCLoggingCategory.h" + +#include + +QGC_LOGGING_CATEGORY(ObjectItemModelBaseLog, "API.ObjectItemModelBase") + +ObjectItemModelBase::ObjectItemModelBase(QObject* parent) + : QAbstractItemModel(parent) +{ + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); +} + +ObjectItemModelBase::~ObjectItemModelBase() +{ + if (_resetModelNestingCount > 0) { + qCWarning(ObjectItemModelBaseLog) << "Destroyed with unbalanced nesting of begin/endResetModel calls - _resetModelNestingCount:" << _resetModelNestingCount << this; + } +} + +QHash ObjectItemModelBase::roleNames() const +{ + return { + { ObjectRole, "object" }, + { TextRole, "text" }, + }; +} + +void ObjectItemModelBase::_childDirtyChanged(bool dirty) +{ + _dirty |= dirty; + emit dirtyChanged(_dirty); +} + +void ObjectItemModelBase::beginResetModel() +{ + if (_resetModelNestingCount == 0) { + qCDebug(ObjectItemModelBaseLog) << "First beginResetModel - calling QAbstractItemModel::beginResetModel" << this; + QAbstractItemModel::beginResetModel(); + } + _resetModelNestingCount++; + qCDebug(ObjectItemModelBaseLog) << "Reset model nesting count" << _resetModelNestingCount << this; +} + +void ObjectItemModelBase::endResetModel() +{ + if (_resetModelNestingCount == 0) { + qCWarning(ObjectItemModelBaseLog) << "endResetModel called without prior beginResetModel"; + return; + } + _resetModelNestingCount--; + qCDebug(ObjectItemModelBaseLog) << "Reset model nesting count" << _resetModelNestingCount << this; + if (_resetModelNestingCount == 0) { + qCDebug(ObjectItemModelBaseLog) << "Last endResetModel - calling QAbstractItemModel::endResetModel" << this; + QAbstractItemModel::endResetModel(); + emit countChanged(count()); + } +} + +void ObjectItemModelBase::_signalCountChangedIfNotNested() +{ + if (_resetModelNestingCount == 0) { + emit countChanged(count()); + } +} diff --git a/src/QmlControls/ObjectItemModelBase.h b/src/QmlControls/ObjectItemModelBase.h new file mode 100644 index 000000000000..40f5310f0cc8 --- /dev/null +++ b/src/QmlControls/ObjectItemModelBase.h @@ -0,0 +1,60 @@ +/**************************************************************************** + * + * (c) 2009-2024 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +#pragma once + +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(ObjectItemModelBaseLog) + +/// Common base for QObject*-based item models (flat lists and trees). +/// Provides: dirty tracking, depth-counted begin/endResetModel, shared role constants, roleNames. +class ObjectItemModelBase : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit ObjectItemModelBase(QObject* parent = nullptr); + ~ObjectItemModelBase() override; + + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) + + bool dirty() const { return _dirty; } + + /// Depth-counted beginResetModel — only the outermost call has effect. + void beginResetModel(); + + /// Depth-counted endResetModel — only the outermost call has effect. + void endResetModel(); + + virtual int count() const = 0; + virtual bool isEmpty() const { return (count() == 0); } + virtual void setDirty(bool dirty) = 0; + virtual void clear() = 0; + +signals: + void countChanged(int count); + void dirtyChanged(bool dirty); + +protected slots: + void _childDirtyChanged(bool dirty); + +protected: + QHash roleNames() const override; + void _signalCountChangedIfNotNested(); + + bool _dirty = false; + uint _resetModelNestingCount = 0; + + static constexpr int ObjectRole = Qt::UserRole; + static constexpr int TextRole = Qt::UserRole + 1; +}; diff --git a/src/QmlControls/ObjectListModelBase.cc b/src/QmlControls/ObjectListModelBase.cc index 9e184344fb67..f38f4a0ecdd5 100644 --- a/src/QmlControls/ObjectListModelBase.cc +++ b/src/QmlControls/ObjectListModelBase.cc @@ -10,72 +10,41 @@ #include "ObjectListModelBase.h" #include "QGCLoggingCategory.h" -#include -#include - QGC_LOGGING_CATEGORY(ObjectListModelBaseLog, "API.ObjectListModelBase") ObjectListModelBase::ObjectListModelBase(QObject* parent) - : QAbstractListModel (parent) - , _dirty (false) - , _skipDirtyFirstItem (false) + : ObjectItemModelBase(parent) { - QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } ObjectListModelBase::~ObjectListModelBase() { - if (_resetModelNestingCount > 0) { - qCWarning(ObjectListModelBaseLog) << "Destroyed with unbalanced nesting of begin/endResetModel calls - _resetModelNestingCount:" << _resetModelNestingCount << this; - } } -QHash ObjectListModelBase::roleNames(void) const -{ - QHash hash; - - hash[ObjectRole] = "object"; - hash[TextRole] = "text"; - - return hash; -} +// Flat-list overrides — same semantics as QAbstractListModel -void ObjectListModelBase::_childDirtyChanged(bool dirty) +QModelIndex ObjectListModelBase::index(int row, int column, const QModelIndex& parent) const { - _dirty |= dirty; - // We want to emit dirtyChanged even if the actual value of _dirty didn't change. It can be a useful - // signal to know when a child has changed dirty state - emit dirtyChanged(_dirty); + if (parent.isValid() || column != 0 || row < 0 || row >= rowCount()) { + return {}; + } + return createIndex(row, 0); } -void ObjectListModelBase::beginResetModel() +QModelIndex ObjectListModelBase::parent(const QModelIndex& child) const { - if (_resetModelNestingCount == 0) { - qCDebug(ObjectListModelBaseLog) << "First call to begiResetModel - calling QAbstractListModel::beginResetModel" << this; - QAbstractListModel::beginResetModel(); - } - _resetModelNestingCount++; - qCDebug(ObjectListModelBaseLog) << "Reset model nesting count" << _resetModelNestingCount << this; + Q_UNUSED(child); + return {}; } -void ObjectListModelBase::endResetModel() +int ObjectListModelBase::columnCount(const QModelIndex& parent) const { - if (_resetModelNestingCount == 0) { - qCWarning(ObjectListModelBaseLog) << "Internal Error: endResetModel called without prior beginResetModel"; - return; - } - _resetModelNestingCount--; - qCDebug(ObjectListModelBaseLog) << "Reset model nesting count" << _resetModelNestingCount << this; - if (_resetModelNestingCount == 0) { - qCDebug(ObjectListModelBaseLog) << "Last call to endResetModel - calling QAbstractListModel::endResetModel" << this; - QAbstractListModel::endResetModel(); - emit countChanged(count()); - } + Q_UNUSED(parent); + return 1; } -void ObjectListModelBase::_signalCountChangedIfNotNested() +bool ObjectListModelBase::hasChildren(const QModelIndex& parent) const { - if (_resetModelNestingCount == 0) { - emit countChanged(count()); - } + // Only the root (invalid parent) has children in a flat list + return !parent.isValid() && rowCount() > 0; } diff --git a/src/QmlControls/ObjectListModelBase.h b/src/QmlControls/ObjectListModelBase.h index 34e4ee9c7a0d..320c0e642e36 100644 --- a/src/QmlControls/ObjectListModelBase.h +++ b/src/QmlControls/ObjectListModelBase.h @@ -10,61 +10,37 @@ #pragma once -#include -#include -#include +#include "ObjectItemModelBase.h" Q_DECLARE_LOGGING_CATEGORY(ObjectListModelBaseLog) -/// Base class for custom object list models: QmlObjectListModel, SparselObjectListModel -class ObjectListModelBase : public QAbstractListModel +/// Base class for flat QObject* list models. Inherits common dirty/reset/role +/// handling from ObjectItemModelBase and adds flat-list index()/parent() overrides. +class ObjectListModelBase : public ObjectItemModelBase { Q_OBJECT public: ObjectListModelBase(QObject* parent = nullptr); - ~ObjectListModelBase(); + ~ObjectListModelBase() override; - Q_PROPERTY(int count READ count NOTIFY countChanged) - - /// Returns true if any of the items in the list are dirty. Requires each object to have - /// a dirty property and dirtyChanged signal. - Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) - - bool dirty() const { return _dirty; } - void beginResetModel(); ///< Supported nesting of calls such that only outermost call has effect - void endResetModel(); ///< Supported nesting of calls such that only outermost call has effect - - virtual int count() const = 0; - virtual bool isEmpty() const { return (count() == 0); } - virtual void setDirty(bool dirty) = 0; - virtual void clear() = 0; virtual void clearAndDeleteContents() = 0; ///< Clears the list and calls deleteLater on each entry virtual QObject* removeOne(const QObject* object) = 0; virtual bool contains(const QObject* object) = 0; -signals: - void countChanged(int count); - void dirtyChanged(bool dirtyChanged); - -protected slots: - void _childDirtyChanged(bool dirty); + // Flat-list overrides of QAbstractItemModel — same behavior as QAbstractListModel + QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& child) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + bool hasChildren(const QModelIndex& parent = QModelIndex()) const override; protected: - // Overrides from QAbstractListModel which must be implemented by derived classes - int rowCount(const QModelIndex & parent = QModelIndex()) const override = 0; + // Overrides from QAbstractItemModel which must be implemented by derived classes + int rowCount(const QModelIndex& parent = QModelIndex()) const override = 0; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override = 0; bool insertRows(int position, int rows, const QModelIndex& index = QModelIndex()) override = 0; bool removeRows(int position, int rows, const QModelIndex& index = QModelIndex()) override = 0; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override = 0; - void _signalCountChangedIfNotNested(); - - QHash roleNames(void) const override; - bool _dirty; - bool _skipDirtyFirstItem; - uint _resetModelNestingCount = 0; - - static constexpr int ObjectRole = Qt::UserRole; - static constexpr int TextRole = Qt::UserRole + 1; + bool _skipDirtyFirstItem = false; }; diff --git a/src/QmlControls/PlanViewRightPanel.qml b/src/QmlControls/PlanViewRightPanel.qml index 0866393a361a..1bd7056621d2 100644 --- a/src/QmlControls/PlanViewRightPanel.qml +++ b/src/QmlControls/PlanViewRightPanel.qml @@ -11,22 +11,11 @@ Item { id: root - // These must match the indices of _editingToolComponents - readonly property int _editingToolMission: 0 - readonly property int _editingToolFence: 1 - readonly property int _editingToolRally: 2 - - property int _editingTool: _editingToolMission - property var _missionController: planMasterController.missionController - property var _geoFenceController: planMasterController.geoFenceController - property var _rallyPointController: planMasterController.rallyPointController - property var _visualItems: _missionController.visualItems - property var _editingToolComponents: [ missionToolComponent, fenceToolComponent, rallyToolComponent ] - property real _toolsMargin: ScreenTools.defaultFontPixelWidth * 0.75 + property var _missionController: planMasterController.missionController + property real _toolsMargin: ScreenTools.defaultFontPixelWidth * 0.75 function selectNextNotReady() { - var foundCurrent = false - for (var i=0; i<_missionController.visualItems.count; i++) { + for (var i = 0; i < _missionController.visualItems.count; i++) { var vmi = _missionController.visualItems.get(i) if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) { _missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true) @@ -99,112 +88,10 @@ Item { anchors.fill: parent } - ColumnLayout { - anchors.fill: parent - spacing: ScreenTools.defaultFontPixelHeight * 0.5 - - QGCTabBar { - Layout.fillWidth: true - - QGCTabButton { - text: qsTr("Mission") - onClicked: { root._editingTool = root._editingToolMission; _editingLayer = _layerMission } - } - - QGCTabButton { - text: qsTr("Fence") - onClicked: { root._editingTool = root._editingToolFence; _editingLayer = _layerFence } - } - - QGCTabButton { - text: qsTr("Rally") - onClicked: { root._editingTool = root._editingToolRally; _editingLayer = _layerRally } - } - } - - Loader { - id: editingToolLoader - Layout.fillWidth: true - Layout.fillHeight: true - sourceComponent: root._editingToolComponents[root._editingTool] - } - } - - Component { - id: missionToolComponent - - ColumnLayout { - spacing: ScreenTools.defaultFontPixelHeight / 2 - - QGCListView { - id: missionItemEditorListView - Layout.fillWidth: true - Layout.fillHeight: true - spacing: ScreenTools.defaultFontPixelHeight / 4 - orientation: ListView.Vertical - model: _missionController.visualItems - cacheBuffer: Math.max(height * 2, 0) - clip: true - currentIndex: _missionController.currentPlanViewSeqNum - highlightMoveDuration: 250 - - delegate: MissionItemEditor { - map: editorMap - masterController: planMasterController - missionItem: object - width: missionItemEditorListView.width - readOnly: false - - onClicked: _missionController.setCurrentPlanViewSeqNum(object.sequenceNumber, false) - - onRemove: { - var removeVIIndex = index - _missionController.removeVisualItem(removeVIIndex) - if (removeVIIndex >= _missionController.visualItems.count) { - removeVIIndex-- - } - } - - onSelectNextNotReadyItem: selectNextNotReady() - } - } - } + MissionItemTreeView { + anchors.fill: parent + editorMap: root.editorMap + planMasterController: root.planMasterController } - - Component { - id: fenceToolComponent - - Column { - spacing: ScreenTools.defaultFontPixelHeight / 2 - - GeoFenceEditor { - width: parent.width - myGeoFenceController: root._geoFenceController - flightMap: root.editorMap - } - } - } - - - Component { - id: rallyToolComponent - - Column { - spacing: ScreenTools.defaultFontPixelHeight / 2 - - RallyPointEditorHeader { - width: parent.width - controller: root._rallyPointController - } - - RallyPointItemEditor { - width: parent.width - visible: root._rallyPointController.points.count - rallyPoint: root._rallyPointController.currentRallyPoint - controller: root._rallyPointController - } - } - } - } } diff --git a/src/QmlControls/QmlObjectTreeModel.cc b/src/QmlControls/QmlObjectTreeModel.cc new file mode 100644 index 000000000000..ac1015f30244 --- /dev/null +++ b/src/QmlControls/QmlObjectTreeModel.cc @@ -0,0 +1,513 @@ +/**************************************************************************** + * + * (c) 2009-2024 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +#include "QmlObjectTreeModel.h" + +#include +#include + +#include "QGCLoggingCategory.h" + +QGC_LOGGING_CATEGORY(QmlObjectTreeModelLog, "API.QmlObjectTreeModel") + +namespace { + +constexpr const char* kDirtyChangedSignature = "dirtyChanged(bool)"; +constexpr const char* kChildDirtyChangedSlotSignature = "_childDirtyChanged(bool)"; + +QMetaMethod childDirtyChangedSlot() +{ + const QMetaObject* metaObject = &QmlObjectTreeModel::staticMetaObject; + const int slotIndex = metaObject->indexOfSlot(kChildDirtyChangedSlotSignature); + Q_ASSERT_X(slotIndex >= 0, "childDirtyChangedSlot", "slot signature mismatch — update kChildDirtyChangedSlotSignature"); + return (slotIndex >= 0) ? metaObject->method(slotIndex) : QMetaMethod(); +} + +QMetaMethod dirtyChangedSignal(const QObject* object) +{ + if (!object) { + return QMetaMethod(); + } + + const int signalIndex = object->metaObject()->indexOfSignal(kDirtyChangedSignature); + return (signalIndex >= 0) ? object->metaObject()->method(signalIndex) : QMetaMethod(); +} + +} // namespace + +//----------------------------------------------------------------------------- +// TreeNode +//----------------------------------------------------------------------------- + +int QmlObjectTreeModel::TreeNode::row() const +{ + if (parentNode) { + return parentNode->children.indexOf(const_cast(this)); + } + return 0; +} + +//----------------------------------------------------------------------------- +// Construction / Destruction +//----------------------------------------------------------------------------- + +QmlObjectTreeModel::QmlObjectTreeModel(QObject* parent) + : ObjectItemModelBase(parent) +{ +} + +QmlObjectTreeModel::~QmlObjectTreeModel() +{ + _disconnectSubtree(&_rootNode); + _deleteSubtree(&_rootNode, false); +} + +//----------------------------------------------------------------------------- +// QAbstractItemModel overrides +//----------------------------------------------------------------------------- + +QModelIndex QmlObjectTreeModel::index(int row, int column, const QModelIndex& parent) const +{ + if (column != 0) { + return {}; + } + + const TreeNode* parentNode = parent.isValid() ? _nodeFromIndex(parent) : &_rootNode; + if (!parentNode || row < 0 || row >= parentNode->children.count()) { + return {}; + } + + return createIndex(row, 0, parentNode->children.at(row)); +} + +QModelIndex QmlObjectTreeModel::parent(const QModelIndex& child) const +{ + if (!child.isValid()) { + return {}; + } + + const TreeNode* node = _nodeFromIndex(child); + if (!node || !node->parentNode || node->parentNode == &_rootNode) { + return {}; + } + + return _indexForNode(node->parentNode); +} + +int QmlObjectTreeModel::rowCount(const QModelIndex& parent) const +{ + const TreeNode* node = parent.isValid() ? _nodeFromIndex(parent) : &_rootNode; + return node ? node->children.count() : 0; +} + +int QmlObjectTreeModel::columnCount(const QModelIndex& parent) const +{ + Q_UNUSED(parent); + return 1; +} + +bool QmlObjectTreeModel::hasChildren(const QModelIndex& parent) const +{ + const TreeNode* node = parent.isValid() ? _nodeFromIndex(parent) : &_rootNode; + return node && !node->children.isEmpty(); +} + +QVariant QmlObjectTreeModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + const TreeNode* node = _nodeFromIndex(index); + if (!node) { + return {}; + } + + switch (role) { + case ObjectRole: + return node->object ? QVariant::fromValue(node->object) : QVariant{}; + case TextRole: + return node->object ? QVariant::fromValue(node->object->objectName()) : QVariant{}; + case NodeTypeRole: + return QVariant::fromValue(node->nodeType); + default: + return {}; + } +} + +bool QmlObjectTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (!index.isValid() || role != ObjectRole) { + return false; + } + + TreeNode* node = _nodeFromIndex(index); + if (!node) { + return false; + } + + if (node->object) { + _disconnectDirtyChanged(node->object); + } + + node->object = value.value(); + + if (node->object) { + QQmlEngine::setObjectOwnership(node->object, QQmlEngine::CppOwnership); + _connectDirtyChanged(node->object); + } + + emit dataChanged(index, index); + return true; +} + +bool QmlObjectTreeModel::insertRows(int /*row*/, int /*count*/, const QModelIndex& /*parent*/) +{ + qCWarning(QmlObjectTreeModelLog) << "insertRows() not supported — use insertItem()"; + return false; +} + +bool QmlObjectTreeModel::removeRows(int /*row*/, int /*count*/, const QModelIndex& /*parent*/) +{ + qCWarning(QmlObjectTreeModelLog) << "removeRows() not supported — use removeItem()"; + return false; +} + +QHash QmlObjectTreeModel::roleNames() const +{ + auto roles = ObjectItemModelBase::roleNames(); + roles[NodeTypeRole] = "nodeType"; + return roles; +} + +//----------------------------------------------------------------------------- +// Properties +//----------------------------------------------------------------------------- + +int QmlObjectTreeModel::count() const +{ + return _totalCount; +} + +void QmlObjectTreeModel::setDirty(bool dirty) +{ + if (_dirty != dirty) { + _dirty = dirty; + emit dirtyChanged(_dirty); + } +} + +//----------------------------------------------------------------------------- +// QML-accessible tree operations +//----------------------------------------------------------------------------- + +QObject* QmlObjectTreeModel::getObject(const QModelIndex& index) const +{ + if (!index.isValid()) { + return nullptr; + } + + const TreeNode* node = _nodeFromIndex(index); + return node ? node->object : nullptr; +} + +QModelIndex QmlObjectTreeModel::appendItem(QObject* object, const QModelIndex& parentIndex) +{ + TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode; + if (!parentNode) { + qCWarning(QmlObjectTreeModelLog) << "appendItem: invalid parent index"; + return {}; + } + + return insertItem(parentNode->children.count(), object, parentIndex); +} + +QModelIndex QmlObjectTreeModel::insertItem(int row, QObject* object, const QModelIndex& parentIndex) +{ + return insertItem(row, object, parentIndex, QString()); +} + +QModelIndex QmlObjectTreeModel::insertItem(int row, QObject* object, const QModelIndex& parentIndex, const QString& nodeType) +{ + TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode; + if (!parentNode) { + qCWarning(QmlObjectTreeModelLog) << "insertItem: invalid parent index"; + return {}; + } + + if (row < 0 || row > parentNode->children.count()) { + qCWarning(QmlObjectTreeModelLog) << "insertItem: invalid row" << row << "count:" << parentNode->children.count(); + return {}; + } + + auto* node = new TreeNode; + node->object = object; + node->parentNode = parentNode; + node->nodeType = nodeType; + + if (object) { + QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership); + _connectDirtyChanged(object); + } + + if (_resetModelNestingCount > 0) { + // During a batch reset we just accumulate nodes silently + parentNode->children.insert(row, node); + } else { + beginInsertRows(parentIndex, row, row); + parentNode->children.insert(row, node); + endInsertRows(); + _signalCountChangedIfNotNested(); + } + + _totalCount++; + setDirty(true); + + return createIndex(row, 0, node); +} + +QModelIndex QmlObjectTreeModel::appendItem(QObject* object, const QModelIndex& parentIndex, const QString& nodeType) +{ + TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode; + if (!parentNode) { + qCWarning(QmlObjectTreeModelLog) << "appendItem: invalid parent index"; + return {}; + } + return insertItem(parentNode->children.count(), object, parentIndex, nodeType); +} + +QObject* QmlObjectTreeModel::removeItem(const QModelIndex& index) +{ + if (!index.isValid()) { + qCWarning(QmlObjectTreeModelLog) << "removeItem: invalid index"; + return nullptr; + } + + TreeNode* node = _nodeFromIndex(index); + if (!node || !node->parentNode) { + qCWarning(QmlObjectTreeModelLog) << "removeItem: node not found or is the root"; + return nullptr; + } + + QObject* object = node->object; + TreeNode* parentNode = node->parentNode; + const int row = node->row(); + const QModelIndex parentIdx = _indexForNode(parentNode); + + _disconnectSubtree(node); + + // Count the subtree nodes being removed (the node itself + all descendants) + const int removedCount = 1 + _subtreeCount(node); + + beginRemoveRows(parentIdx, row, row); + parentNode->children.removeAt(row); + endRemoveRows(); + + // Free the subtree's TreeNode objects but NOT the QObjects + _deleteSubtree(node, false); + delete node; + + _totalCount -= removedCount; + _signalCountChangedIfNotNested(); + setDirty(true); + + return object; +} + +QModelIndex QmlObjectTreeModel::indexForObject(QObject* object) const +{ + if (!object) { + return {}; + } + + const TreeNode* node = _findNode(&_rootNode, object); + return node ? _indexForNode(node) : QModelIndex(); +} + +int QmlObjectTreeModel::depth(const QModelIndex& index) const +{ + if (!index.isValid()) { + return -1; + } + + int d = 0; + const TreeNode* node = _nodeFromIndex(index); + while (node && node->parentNode && node->parentNode != &_rootNode) { + d++; + node = node->parentNode; + } + return d; +} + +//----------------------------------------------------------------------------- +// C++ convenience API +//----------------------------------------------------------------------------- + +void QmlObjectTreeModel::appendRootItem(QObject* object) +{ + appendItem(object); +} + +void QmlObjectTreeModel::appendChild(const QModelIndex& parentIndex, QObject* object) +{ + appendItem(object, parentIndex); +} + +QObject* QmlObjectTreeModel::removeAt(const QModelIndex& parentIndex, int row) +{ + return removeItem(index(row, 0, parentIndex)); +} + +void QmlObjectTreeModel::removeChildren(const QModelIndex& parentIndex) +{ + TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode; + if (!parentNode || parentNode->children.isEmpty()) { + return; + } + + const int childCount = parentNode->children.count(); + + // Count all nodes being removed (direct children + their subtrees) + int removedCount = childCount; + for (const TreeNode* child : parentNode->children) { + removedCount += _subtreeCount(child); + } + + if (_resetModelNestingCount == 0) { + beginRemoveRows(parentIndex, 0, childCount - 1); + } + + for (TreeNode* child : parentNode->children) { + _disconnectSubtree(child); + _deleteSubtree(child, false); + delete child; + } + parentNode->children.clear(); + + _totalCount -= removedCount; + + if (_resetModelNestingCount == 0) { + endRemoveRows(); + _signalCountChangedIfNotNested(); + } +} + +void QmlObjectTreeModel::clear() +{ + if (_rootNode.children.isEmpty()) { + return; + } + + beginResetModel(); + _disconnectSubtree(&_rootNode); + _deleteSubtree(&_rootNode, false); + _totalCount = 0; + endResetModel(); +} + +void QmlObjectTreeModel::clearAndDeleteContents() +{ + if (_rootNode.children.isEmpty()) { + return; + } + + beginResetModel(); + _disconnectSubtree(&_rootNode); + _deleteSubtree(&_rootNode, true); + _totalCount = 0; + endResetModel(); +} + +bool QmlObjectTreeModel::contains(QObject* object) const +{ + return _findNode(&_rootNode, object) != nullptr; +} + +//----------------------------------------------------------------------------- +// Private helpers +//----------------------------------------------------------------------------- + +QmlObjectTreeModel::TreeNode* QmlObjectTreeModel::_nodeFromIndex(const QModelIndex& index) const +{ + if (!index.isValid()) { + return nullptr; + } + return static_cast(index.internalPointer()); +} + +QModelIndex QmlObjectTreeModel::_indexForNode(const TreeNode* node) const +{ + if (!node || node == &_rootNode) { + return {}; + } + return createIndex(node->row(), 0, const_cast(node)); +} + +QmlObjectTreeModel::TreeNode* QmlObjectTreeModel::_findNode(const TreeNode* root, const QObject* object) const +{ + for (TreeNode* child : root->children) { + if (child->object == object) { + return child; + } + TreeNode* found = _findNode(child, object); + if (found) { + return found; + } + } + return nullptr; +} + +int QmlObjectTreeModel::_subtreeCount(const TreeNode* node) +{ + int result = node->children.count(); + for (const TreeNode* child : node->children) { + result += _subtreeCount(child); + } + return result; +} + +void QmlObjectTreeModel::_disconnectSubtree(TreeNode* node) +{ + if (node != &_rootNode && node->object) { + _disconnectDirtyChanged(node->object); + } + for (TreeNode* child : node->children) { + _disconnectSubtree(child); + } +} + +void QmlObjectTreeModel::_deleteSubtree(TreeNode* node, bool deleteObjects) +{ + for (TreeNode* child : node->children) { + _deleteSubtree(child, deleteObjects); + if (deleteObjects && child->object) { + child->object->deleteLater(); + } + delete child; + } + node->children.clear(); +} + +void QmlObjectTreeModel::_connectDirtyChanged(QObject* object) +{ + const QMetaMethod signal = dirtyChangedSignal(object); + const QMetaMethod slot = childDirtyChangedSlot(); + if (signal.isValid() && slot.isValid()) { + connect(object, signal, this, slot); + } +} + +void QmlObjectTreeModel::_disconnectDirtyChanged(QObject* object) +{ + const QMetaMethod signal = dirtyChangedSignal(object); + const QMetaMethod slot = childDirtyChangedSlot(); + if (signal.isValid() && slot.isValid()) { + disconnect(object, signal, this, slot); + } +} diff --git a/src/QmlControls/QmlObjectTreeModel.h b/src/QmlControls/QmlObjectTreeModel.h new file mode 100644 index 000000000000..48e6bd7ca9f7 --- /dev/null +++ b/src/QmlControls/QmlObjectTreeModel.h @@ -0,0 +1,120 @@ +/**************************************************************************** + * + * (c) 2009-2024 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +#pragma once + +#include "ObjectItemModelBase.h" + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(QmlObjectTreeModelLog) + +/// A tree model for QObject* items, usable from both C++ and QML. +/// Works like QmlObjectListModel but supports hierarchical parent/child relationships. +/// Compatible with Qt 6 TreeView. +/// +/// Top-level items are children of an invisible root node. The root is represented +/// by an invalid QModelIndex (the default). +class QmlObjectTreeModel : public ObjectItemModelBase +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + +public: + explicit QmlObjectTreeModel(QObject* parent = nullptr); + ~QmlObjectTreeModel() override; + + // -- ObjectItemModelBase overrides -- + int count() const override; + void setDirty(bool dirty) override; + void clear() override; + + // -- QAbstractItemModel overrides -- + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& child) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + bool hasChildren(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; + QHash roleNames() const override; + + // -- QML-accessible tree operations -- + + /// Returns the QObject* stored at @p index, or nullptr if invalid + Q_INVOKABLE QObject* getObject(const QModelIndex& index) const; + + /// Appends @p object as the last child of @p parentIndex (root if invalid). Returns the new item's index. + Q_INVOKABLE QModelIndex appendItem(QObject* object, const QModelIndex& parentIndex = QModelIndex()); + + /// Same as above but also sets a nodeType tag on the created tree node. + Q_INVOKABLE QModelIndex appendItem(QObject* object, const QModelIndex& parentIndex, const QString& nodeType); + + /// Inserts @p object at @p row under @p parentIndex. Returns the new item's index. + Q_INVOKABLE QModelIndex insertItem(int row, QObject* object, const QModelIndex& parentIndex = QModelIndex()); + + /// Same as above but also sets a nodeType tag on the created tree node. + Q_INVOKABLE QModelIndex insertItem(int row, QObject* object, const QModelIndex& parentIndex, const QString& nodeType); + + /// Removes the item (and its entire subtree) at @p index. Returns the removed QObject* (caller takes ownership). + /// Child QObjects are NOT deleted; only the internal tree nodes are freed. + Q_INVOKABLE QObject* removeItem(const QModelIndex& index); + + /// Number of direct children under @p parentIndex + Q_INVOKABLE int childCount(const QModelIndex& parentIndex = QModelIndex()) const { return rowCount(parentIndex); } + + /// Returns the QModelIndex for the child at @p row under @p parentIndex + Q_INVOKABLE QModelIndex childIndex(int row, const QModelIndex& parentIndex = QModelIndex()) const { return index(row, 0, parentIndex); } + + /// Searches the entire tree for @p object and returns its QModelIndex (invalid if not found) + Q_INVOKABLE QModelIndex indexForObject(QObject* object) const; + + /// Convenience wrapper around parent() + Q_INVOKABLE QModelIndex parentIndex(const QModelIndex& index) const { return parent(index); } + + /// Returns the depth of @p index (0 = root-level item, -1 = invalid index) + Q_INVOKABLE int depth(const QModelIndex& index) const; + + // -- C++ convenience API -- + void appendRootItem(QObject* object); + void appendChild(const QModelIndex& parentIndex, QObject* object); + QObject* removeAt(const QModelIndex& parentIndex, int row); + void removeChildren(const QModelIndex& parentIndex); ///< Removes all children of parentIndex without removing the parent itself + void clearAndDeleteContents(); ///< Clears the tree and calls deleteLater on every QObject + bool contains(QObject* object) const; + + static constexpr int NodeTypeRole = Qt::UserRole + 2; + +private: + struct TreeNode { + QObject* object = nullptr; + TreeNode* parentNode = nullptr; + QList children; + QString nodeType; ///< Discriminator tag for delegates (e.g. "missionGroup", "fenceEditor") + + /// Row of this node within its parent's children list + int row() const; + }; + + TreeNode* _nodeFromIndex(const QModelIndex& index) const; + QModelIndex _indexForNode(const TreeNode* node) const; + TreeNode* _findNode(const TreeNode* root, const QObject* object) const; + static int _subtreeCount(const TreeNode* node); + void _disconnectSubtree(TreeNode* node); + void _deleteSubtree(TreeNode* node, bool deleteObjects); + void _connectDirtyChanged(QObject* object); + void _disconnectDirtyChanged(QObject* object); + + TreeNode _rootNode; ///< Invisible root; top-level items are its children + int _totalCount = 0; ///< Cached total node count (all nodes in tree) +}; From 9d3ce2d26c1c78c500c852b368f851bfddc4db71 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 5 Mar 2026 18:20:21 -0800 Subject: [PATCH 030/119] Add unit tests for PlanTreeView model classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unit test suites: - QmlObjectTreeModelTest (48 tests): insert/remove/clear, tree navigation, data roles, dirty tracking, model signals, nested reset guards, persistent index stability, and null object edge cases. - ObjectItemModelBaseTest (10 tests): nested beginResetModel/endResetModel at depths 1-3, count signal suppression during reset, sticky dirty tracking via _childDirtyChanged, and roleNames verification. - ObjectListModelBaseTest (8 tests): validates QAbstractItemModel contract for flat lists — index bounds, column must be 0, parent always invalid, columnCount always 1, hasChildren correct for root vs non-root. - MissionControllerTreeTest (6 integration tests): verifies tree model stays in sync with visualItems after init, insert, remove, removeAll; validates persistent group indexes survive mutations; exercises recalcChildItems without crash. --- test/CMakeLists.txt | 4 + test/MissionManager/CMakeLists.txt | 1 + .../MissionControllerTreeTest.cc | 256 +++++++ .../MissionControllerTreeTest.h | 46 ++ test/QmlControls/CMakeLists.txt | 6 + test/QmlControls/ObjectItemModelBaseTest.cc | 264 +++++++ test/QmlControls/ObjectItemModelBaseTest.h | 29 + test/QmlControls/ObjectListModelBaseTest.cc | 141 ++++ test/QmlControls/ObjectListModelBaseTest.h | 28 + test/QmlControls/QmlObjectTreeModelTest.cc | 685 ++++++++++++++++++ test/QmlControls/QmlObjectTreeModelTest.h | 84 +++ 11 files changed, 1544 insertions(+) create mode 100644 test/MissionManager/MissionControllerTreeTest.cc create mode 100644 test/MissionManager/MissionControllerTreeTest.h create mode 100644 test/QmlControls/ObjectItemModelBaseTest.cc create mode 100644 test/QmlControls/ObjectItemModelBaseTest.h create mode 100644 test/QmlControls/ObjectListModelBaseTest.cc create mode 100644 test/QmlControls/ObjectListModelBaseTest.h create mode 100644 test/QmlControls/QmlObjectTreeModelTest.cc create mode 100644 test/QmlControls/QmlObjectTreeModelTest.h diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 445025bda190..53de62a8f5d6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -150,6 +150,7 @@ add_qgc_test(MissionCommandTreeEditorTest LABELS Unit MissionManager TIMEOUT ${Q add_qgc_test(MissionCommandTreeTest LABELS Unit MissionManager) add_qgc_test(MissionControllerManagerTest LABELS Integration MissionManager RESOURCE_LOCK MockLink) add_qgc_test(MissionControllerTest LABELS Integration MissionManager RESOURCE_LOCK MockLink) +add_qgc_test(MissionControllerTreeTest LABELS Integration MissionManager RESOURCE_LOCK MockLink) add_qgc_test(MissionItemTest LABELS Unit MissionManager) add_qgc_test(MissionManagerTest LABELS Integration MissionManager SERIAL) add_qgc_test(MissionSettingsTest LABELS Unit MissionManager) @@ -180,7 +181,10 @@ add_qgc_test(UrlFactoryTest LABELS Unit) # QmlControls # ---------------------------------------------------------------------------- add_subdirectory(QmlControls) +add_qgc_test(ObjectItemModelBaseTest LABELS Unit) +add_qgc_test(ObjectListModelBaseTest LABELS Unit) add_qgc_test(QmlObjectListModelTest LABELS Unit) +add_qgc_test(QmlObjectTreeModelTest LABELS Unit) # ---------------------------------------------------------------------------- # Terrain diff --git a/test/MissionManager/CMakeLists.txt b/test/MissionManager/CMakeLists.txt index d46e497396cb..57f7dab4f53c 100644 --- a/test/MissionManager/CMakeLists.txt +++ b/test/MissionManager/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(${CMAKE_PROJECT_NAME} MissionCommandTreeTest.cc MissionCommandTreeTest.h MissionControllerManagerTest.cc MissionControllerManagerTest.h MissionControllerTest.cc MissionControllerTest.h + MissionControllerTreeTest.cc MissionControllerTreeTest.h MissionItemTest.cc MissionItemTest.h MissionManagerTest.cc MissionManagerTest.h MissionSettingsTest.cc MissionSettingsTest.h diff --git a/test/MissionManager/MissionControllerTreeTest.cc b/test/MissionManager/MissionControllerTreeTest.cc new file mode 100644 index 000000000000..475b09833971 --- /dev/null +++ b/test/MissionManager/MissionControllerTreeTest.cc @@ -0,0 +1,256 @@ +#include "MissionControllerTreeTest.h" + +#include "AppSettings.h" +#include "MissionController.h" +#include "MissionSettingsItem.h" +#include "PlanMasterController.h" +#include "QmlObjectTreeModel.h" +#include "SettingsManager.h" +#include "SimpleMissionItem.h" +#include "TestFixtures.h" + +using namespace TestFixtures; + +MissionControllerTreeTest::~MissionControllerTreeTest() = default; + +void MissionControllerTreeTest::cleanup() +{ + _masterController.reset(); + _missionController = nullptr; + MissionControllerManagerTest::cleanup(); +} + +void MissionControllerTreeTest::_initForTest() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + SettingsManager::instance()->appSettings()->offlineEditingFirmwareClass()->setRawValue( + QGCMAVLink::firmwareClass(MAV_AUTOPILOT_PX4)); + + _masterController = std::make_unique(); + _masterController->setFlyView(false); + _missionController = _masterController->missionController(); + + SignalSpyFixture spy(_missionController); + QVERIFY(spy.spy()); + spy.expect("visualItemsChanged"); + _masterController->start(); + QVERIFY(spy.waitAndVerify(TestTimeout::mediumMs())); + + // Sanity — visualItems has MissionSettingsItem at index 0 + QVERIFY(_missionController->visualItems()); + QCOMPARE(_missionController->visualItems()->count(), 1); +} + +// =========================================================================== +// Tree structure after init +// =========================================================================== + +void MissionControllerTreeTest::_testTreeStructureAfterInit() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + QVERIFY(tree); + + // Root should have 3 top-level groups: Mission Items, GeoFence, Rally Points + QCOMPARE(tree->rowCount(), 3); + + // Verify group node types + const QModelIndex missionGroup = tree->index(0, 0); + const QModelIndex fenceGroup = tree->index(1, 0); + const QModelIndex rallyGroup = tree->index(2, 0); + QVERIFY(missionGroup.isValid()); + QVERIFY(fenceGroup.isValid()); + QVERIFY(rallyGroup.isValid()); + + QCOMPARE(tree->data(missionGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("missionGroup")); + QCOMPARE(tree->data(fenceGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("fenceGroup")); + QCOMPARE(tree->data(rallyGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("rallyGroup")); + + // Mission group should contain 1 child (the MissionSettingsItem) + QCOMPARE(tree->rowCount(missionGroup), 1); + + // Fence group should have 1 child (fenceEditor marker) + QCOMPARE(tree->rowCount(fenceGroup), 1); + const QModelIndex fenceChild = tree->index(0, 0, fenceGroup); + QCOMPARE(tree->data(fenceChild, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("fenceEditor")); + + // Rally group should have 1 child (rallyHeader marker) + QCOMPARE(tree->rowCount(rallyGroup), 1); + const QModelIndex rallyChild = tree->index(0, 0, rallyGroup); + QCOMPARE(tree->data(rallyChild, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("rallyHeader")); +} + +// =========================================================================== +// Insert waypoints → tree sync +// =========================================================================== + +void MissionControllerTreeTest::_testInsertWaypointUpdatesTree() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + const QModelIndex missionGroup = tree->index(0, 0); + + // Before insert: 1 mission item (MissionSettingsItem) + const int initialMissionChildren = tree->rowCount(missionGroup); + QCOMPARE(initialMissionChildren, _missionController->visualItems()->count()); + + // Insert 3 waypoints + const QList waypoints = Coord::waypointPath(Coord::zurich(), 3); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + // _visualItems should have 4 items (settings + 3 waypoints) + QCOMPARE(_missionController->visualItems()->count(), 4); + + // Tree's mission group should mirror that count + QCOMPARE(tree->rowCount(missionGroup), 4); + + // Verify tree children correspond to visualItems + for (int i = 0; i < _missionController->visualItems()->count(); i++) { + QObject* listObj = _missionController->visualItems()->get(i); + const QModelIndex treeChild = tree->index(i, 0, missionGroup); + QObject* treeObj = tree->data(treeChild, Qt::UserRole).value(); + QCOMPARE(treeObj, listObj); + } +} + +// =========================================================================== +// Remove waypoints → tree sync +// =========================================================================== + +void MissionControllerTreeTest::_testRemoveWaypointUpdatesTree() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + const QModelIndex missionGroup = tree->index(0, 0); + + // Insert 3 waypoints + const QList waypoints = Coord::waypointPath(Coord::zurich(), 3); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + QCOMPARE(tree->rowCount(missionGroup), 4); // settings + 3 waypoints + + // Remove middle waypoint (visualItem index 2) + _missionController->removeVisualItem(2); + + QCOMPARE(_missionController->visualItems()->count(), 3); + QCOMPARE(tree->rowCount(missionGroup), 3); + + // Verify correspondence + for (int i = 0; i < _missionController->visualItems()->count(); i++) { + QObject* listObj = _missionController->visualItems()->get(i); + const QModelIndex treeChild = tree->index(i, 0, missionGroup); + QObject* treeObj = tree->data(treeChild, Qt::UserRole).value(); + QCOMPARE(treeObj, listObj); + } +} + +// =========================================================================== +// removeAll → tree rebuilt +// =========================================================================== + +void MissionControllerTreeTest::_testRemoveAllRebuildsTree() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + + // Insert waypoints + const QList waypoints = Coord::waypointPath(Coord::zurich(), 3); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + const QModelIndex missionGroupBefore = tree->index(0, 0); + QCOMPARE(tree->rowCount(missionGroupBefore), 4); + + // Remove all + _missionController->removeAll(); + + // After removeAll, tree should still have 3 groups + QCOMPARE(tree->rowCount(), 3); + + // Mission group should have 1 child (the new MissionSettingsItem) + const QModelIndex missionGroupAfter = tree->index(0, 0); + QCOMPARE(tree->rowCount(missionGroupAfter), 1); + + // That child should be the new settings item + const QModelIndex settingsChild = tree->index(0, 0, missionGroupAfter); + QObject* settingsObj = tree->data(settingsChild, Qt::UserRole).value(); + QCOMPARE(settingsObj, static_cast(_missionController->visualItems()->get(0))); +} + +// =========================================================================== +// Persistent group indexes remain valid across operations +// =========================================================================== + +void MissionControllerTreeTest::_testPersistentGroupIndexes() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + + // Capture persistent indexes for each group + QPersistentModelIndex missionPersist(tree->index(0, 0)); + QPersistentModelIndex fencePersist(tree->index(1, 0)); + QPersistentModelIndex rallyPersist(tree->index(2, 0)); + + // Insert waypoints + const QList waypoints = Coord::waypointPath(Coord::zurich(), 5); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + // All persistent indexes should still be valid + QVERIFY(missionPersist.isValid()); + QVERIFY(fencePersist.isValid()); + QVERIFY(rallyPersist.isValid()); + + // They should still be at the same rows (group structure didn't change) + QCOMPARE(missionPersist.row(), 0); + QCOMPARE(fencePersist.row(), 1); + QCOMPARE(rallyPersist.row(), 2); + + // Remove some waypoints + _missionController->removeVisualItem(3); + _missionController->removeVisualItem(2); + + // Persistent indexes should still be valid + QVERIFY(missionPersist.isValid()); + QVERIFY(fencePersist.isValid()); + QVERIFY(rallyPersist.isValid()); +} + +// =========================================================================== +// recalcChildItems is a no-op for the tree (no crash) +// =========================================================================== + +void MissionControllerTreeTest::_testRecalcChildItemsNoCrash() +{ + _initForTest(); + + // Insert enough waypoints to exercise child item recalc + const QList waypoints = Coord::waypointPath(Coord::zurich(), 4); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + const QModelIndex missionGroup = tree->index(0, 0); + + // Tree should be consistent — recalcChildItems happens automatically during insert + QCOMPARE(tree->rowCount(missionGroup), _missionController->visualItems()->count()); + + // Verify no crash and tree is still functional + QVERIFY(tree->index(0, 0, missionGroup).isValid()); + QCOMPARE(tree->rowCount(), 3); // 3 groups +} + +#include "UnitTest.h" + +UT_REGISTER_TEST(MissionControllerTreeTest, TestLabel::Integration, TestLabel::MissionManager) diff --git a/test/MissionManager/MissionControllerTreeTest.h b/test/MissionManager/MissionControllerTreeTest.h new file mode 100644 index 000000000000..a3134320adcd --- /dev/null +++ b/test/MissionManager/MissionControllerTreeTest.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include "MissionControllerManagerTest.h" + +class MissionController; +class PlanMasterController; + +/// Tests for the MissionController incremental tree model sync (PlanTreeView branch). +/// Verifies that _visualItemsTree stays in sync with _visualItems when +/// inserting/removing waypoints, calling removeAll, and loading plans. +class MissionControllerTreeTest : public MissionControllerManagerTest +{ + Q_OBJECT + +public: + ~MissionControllerTreeTest() override; + +private slots: + void cleanup() override; + + // Tree structure after init + void _testTreeStructureAfterInit(); + + // Insert waypoints → tree sync + void _testInsertWaypointUpdatesTree(); + + // Remove waypoints → tree sync + void _testRemoveWaypointUpdatesTree(); + + // removeAll → tree rebuilt + void _testRemoveAllRebuildsTree(); + + // Multiple inserts keep group persistent indexes valid + void _testPersistentGroupIndexes(); + + // recalcChildItems is a no-op for tree (no crash) + void _testRecalcChildItemsNoCrash(); + +private: + void _initForTest(); + + std::unique_ptr _masterController; + MissionController* _missionController = nullptr; +}; diff --git a/test/QmlControls/CMakeLists.txt b/test/QmlControls/CMakeLists.txt index 8ce8cfd01c64..6cb5355a5fac 100644 --- a/test/QmlControls/CMakeLists.txt +++ b/test/QmlControls/CMakeLists.txt @@ -4,8 +4,14 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE + ObjectItemModelBaseTest.cc + ObjectItemModelBaseTest.h + ObjectListModelBaseTest.cc + ObjectListModelBaseTest.h QmlObjectListModelTest.cc QmlObjectListModelTest.h + QmlObjectTreeModelTest.cc + QmlObjectTreeModelTest.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/test/QmlControls/ObjectItemModelBaseTest.cc b/test/QmlControls/ObjectItemModelBaseTest.cc new file mode 100644 index 000000000000..1ce860591c4b --- /dev/null +++ b/test/QmlControls/ObjectItemModelBaseTest.cc @@ -0,0 +1,264 @@ +#include "ObjectItemModelBaseTest.h" + +#include "ObjectItemModelBase.h" + +#include + +// --------------------------------------------------------------------------- +// Concrete subclass — ObjectItemModelBase is abstract, so we need a minimal +// implementation to test the base-class logic. +// --------------------------------------------------------------------------- +namespace { + +class ConcreteItemModel : public ObjectItemModelBase +{ + Q_OBJECT + +public: + explicit ConcreteItemModel(QObject* parent = nullptr) + : ObjectItemModelBase(parent) + { + } + + // -- ObjectItemModelBase pure-virtuals -- + int count() const override { return _count; } + void setDirty(bool dirty) override + { + if (_dirty == dirty) return; + _dirty = dirty; + emit dirtyChanged(_dirty); + } + void clear() override + { + beginResetModel(); + _count = 0; + endResetModel(); + } + + // -- QAbstractItemModel pure-virtuals -- + QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override + { + Q_UNUSED(parent); + if (row < 0 || row >= _count || column != 0) return {}; + return createIndex(row, 0); + } + QModelIndex parent(const QModelIndex& child) const override + { + Q_UNUSED(child); + return {}; + } + int rowCount(const QModelIndex& parent = QModelIndex()) const override + { + Q_UNUSED(parent); + return _count; + } + int columnCount(const QModelIndex& parent = QModelIndex()) const override + { + Q_UNUSED(parent); + return 1; + } + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + Q_UNUSED(index); Q_UNUSED(role); + return {}; + } + + // Helper to manipulate count from tests + void setCount(int c) { _count = c; } + + // Expose protected helpers for testing + using ObjectItemModelBase::_signalCountChangedIfNotNested; + using ObjectItemModelBase::_childDirtyChanged; + using ObjectItemModelBase::roleNames; + +private: + int _count = 0; +}; + +class DirtyObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) + +public: + explicit DirtyObject(QObject* parent = nullptr) : QObject(parent) {} + + bool dirty() const { return _dirty; } + void setDirty(bool dirty) + { + if (_dirty == dirty) return; + _dirty = dirty; + emit dirtyChanged(_dirty); + } + +signals: + void dirtyChanged(bool dirty); + +private: + bool _dirty = false; +}; + +} // namespace + +// =========================================================================== +// Nested beginResetModel / endResetModel +// =========================================================================== + +void ObjectItemModelBaseTest::_singleResetCallsBaseOnce() +{ + ConcreteItemModel model; + + QSignalSpy resetBeginSpy(&model, &QAbstractItemModel::modelAboutToBeReset); + QSignalSpy resetEndSpy(&model, &QAbstractItemModel::modelReset); + + model.beginResetModel(); + QCOMPARE(resetBeginSpy.count(), 1); + + model.endResetModel(); + QCOMPARE(resetEndSpy.count(), 1); +} + +void ObjectItemModelBaseTest::_nestedResetTwoDeep() +{ + ConcreteItemModel model; + + QSignalSpy resetBeginSpy(&model, &QAbstractItemModel::modelAboutToBeReset); + QSignalSpy resetEndSpy(&model, &QAbstractItemModel::modelReset); + + model.beginResetModel(); + model.beginResetModel(); // nested — should NOT emit again + QCOMPARE(resetBeginSpy.count(), 1); + + model.endResetModel(); // back to depth 1 — should NOT emit yet + QCOMPARE(resetEndSpy.count(), 0); + + model.endResetModel(); // depth 0 — now emit + QCOMPARE(resetEndSpy.count(), 1); +} + +void ObjectItemModelBaseTest::_nestedResetThreeDeep() +{ + ConcreteItemModel model; + + QSignalSpy resetBeginSpy(&model, &QAbstractItemModel::modelAboutToBeReset); + QSignalSpy resetEndSpy(&model, &QAbstractItemModel::modelReset); + + model.beginResetModel(); + model.beginResetModel(); + model.beginResetModel(); // depth 3 + QCOMPARE(resetBeginSpy.count(), 1); + + model.endResetModel(); // depth 2 + model.endResetModel(); // depth 1 + QCOMPARE(resetEndSpy.count(), 0); + + model.endResetModel(); // depth 0 — emit + QCOMPARE(resetEndSpy.count(), 1); +} + +void ObjectItemModelBaseTest::_endResetWithoutBeginLogsWarning() +{ + ConcreteItemModel model; + + QSignalSpy resetEndSpy(&model, &QAbstractItemModel::modelReset); + + // Should just log a warning and not crash + model.endResetModel(); + QCOMPARE(resetEndSpy.count(), 0); +} + +// =========================================================================== +// Count signal suppression during reset +// =========================================================================== + +void ObjectItemModelBaseTest::_countChangedSuppressedInsideReset() +{ + ConcreteItemModel model; + model.setCount(5); + + QSignalSpy countSpy(&model, &ObjectItemModelBase::countChanged); + + model.beginResetModel(); + + // _signalCountChangedIfNotNested should be suppressed while in reset + model._signalCountChangedIfNotNested(); + QCOMPARE(countSpy.count(), 0); + + model.endResetModel(); + // endResetModel itself emits countChanged + QCOMPARE(countSpy.count(), 1); + QCOMPARE(countSpy.takeFirst().at(0).toInt(), 5); +} + +void ObjectItemModelBaseTest::_countChangedEmittedOnEndReset() +{ + ConcreteItemModel model; + model.setCount(3); + + model.beginResetModel(); + model.setCount(7); + + QSignalSpy countSpy(&model, &ObjectItemModelBase::countChanged); + + model.endResetModel(); + QCOMPARE(countSpy.count(), 1); + QCOMPARE(countSpy.takeFirst().at(0).toInt(), 7); +} + +// =========================================================================== +// Dirty tracking via _childDirtyChanged +// =========================================================================== + +void ObjectItemModelBaseTest::_childDirtySetsDirtyTrue() +{ + ConcreteItemModel model; + QCOMPARE(model.dirty(), false); + + model._childDirtyChanged(true); + QCOMPARE(model.dirty(), true); +} + +void ObjectItemModelBaseTest::_childDirtyDoesNotClearDirty() +{ + ConcreteItemModel model; + model._childDirtyChanged(true); + QCOMPARE(model.dirty(), true); + + // _childDirtyChanged(false) should NOT reset the model's dirty flag + // because _dirty |= dirty means it's sticky + model._childDirtyChanged(false); + QCOMPARE(model.dirty(), true); +} + +void ObjectItemModelBaseTest::_childDirtyEmitsDirtyChanged() +{ + ConcreteItemModel model; + + QSignalSpy dirtySpy(&model, &ObjectItemModelBase::dirtyChanged); + + model._childDirtyChanged(true); + QCOMPARE(dirtySpy.count(), 1); + QCOMPARE(dirtySpy.takeFirst().at(0).toBool(), true); + + // Calling again with true — dirty is already true, |= true is still true, + // but the signal is still emitted each time _childDirtyChanged is called + model._childDirtyChanged(true); + QCOMPARE(dirtySpy.count(), 1); +} + +// =========================================================================== +// roleNames +// =========================================================================== + +void ObjectItemModelBaseTest::_roleNamesContainObjectAndText() +{ + ConcreteItemModel model; + const auto roles = model.roleNames(); + + QVERIFY(roles.values().contains("object")); + QVERIFY(roles.values().contains("text")); +} + +UT_REGISTER_TEST(ObjectItemModelBaseTest, TestLabel::Unit) + +#include "ObjectItemModelBaseTest.moc" diff --git a/test/QmlControls/ObjectItemModelBaseTest.h b/test/QmlControls/ObjectItemModelBaseTest.h new file mode 100644 index 000000000000..f5c82ac4d63b --- /dev/null +++ b/test/QmlControls/ObjectItemModelBaseTest.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "UnitTest.h" + +class ObjectItemModelBaseTest : public UnitTest +{ + Q_OBJECT + +private slots: + // Nested beginResetModel / endResetModel + void _singleResetCallsBaseOnce(); + void _nestedResetTwoDeep(); + void _nestedResetThreeDeep(); + void _endResetWithoutBeginLogsWarning(); + + // Count signal suppression during reset + void _countChangedSuppressedInsideReset(); + void _countChangedEmittedOnEndReset(); + + // Dirty tracking via _childDirtyChanged + void _childDirtySetsDirtyTrue(); + void _childDirtyDoesNotClearDirty(); + void _childDirtyEmitsDirtyChanged(); + + // roleNames + void _roleNamesContainObjectAndText(); +}; diff --git a/test/QmlControls/ObjectListModelBaseTest.cc b/test/QmlControls/ObjectListModelBaseTest.cc new file mode 100644 index 000000000000..6eee012e1353 --- /dev/null +++ b/test/QmlControls/ObjectListModelBaseTest.cc @@ -0,0 +1,141 @@ +#include "ObjectListModelBaseTest.h" + +#include "QmlObjectListModel.h" + +// =========================================================================== +// index() overrides +// =========================================================================== + +void ObjectListModelBaseTest::_indexValidRow() +{ + QmlObjectListModel model; + QObject a, b; + model.append(&a); + model.append(&b); + + const QModelIndex idx0 = model.index(0, 0); + QVERIFY(idx0.isValid()); + QCOMPARE(idx0.row(), 0); + QCOMPARE(idx0.column(), 0); + + const QModelIndex idx1 = model.index(1, 0); + QVERIFY(idx1.isValid()); + QCOMPARE(idx1.row(), 1); +} + +void ObjectListModelBaseTest::_indexInvalidRow() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + // Negative row + QVERIFY(!model.index(-1, 0).isValid()); + + // Beyond count + QVERIFY(!model.index(1, 0).isValid()); + QVERIFY(!model.index(100, 0).isValid()); +} + +void ObjectListModelBaseTest::_indexNonZeroColumnInvalid() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + // Column must be 0 for a list model + QVERIFY(!model.index(0, 1).isValid()); + QVERIFY(!model.index(0, 2).isValid()); +} + +void ObjectListModelBaseTest::_indexWithValidParentInvalid() +{ + QmlObjectListModel model; + QObject a, b; + model.append(&a); + model.append(&b); + + const QModelIndex parentIdx = model.index(0, 0); + QVERIFY(parentIdx.isValid()); + + // A flat list model should return invalid for any child of a valid parent + QVERIFY(!model.index(0, 0, parentIdx).isValid()); +} + +// =========================================================================== +// parent() override +// =========================================================================== + +void ObjectListModelBaseTest::_parentAlwaysInvalid() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + const QModelIndex idx = model.index(0, 0); + QVERIFY(idx.isValid()); + + // parent() should always return invalid for a flat list + QVERIFY(!model.parent(idx).isValid()); +} + +// =========================================================================== +// columnCount() override +// =========================================================================== + +void ObjectListModelBaseTest::_columnCountAlwaysOne() +{ + QmlObjectListModel model; + + // Empty model + QCOMPARE(model.columnCount(), 1); + QCOMPARE(model.columnCount(QModelIndex()), 1); + + // With items + QObject a; + model.append(&a); + QCOMPARE(model.columnCount(), 1); + + // With a valid parent index — still 1 + const QModelIndex idx = model.index(0, 0); + QCOMPARE(model.columnCount(idx), 1); +} + +// =========================================================================== +// hasChildren() override +// =========================================================================== + +void ObjectListModelBaseTest::_hasChildrenRootEmpty() +{ + QmlObjectListModel model; + + // Root of an empty list has no children + QVERIFY(!model.hasChildren(QModelIndex())); +} + +void ObjectListModelBaseTest::_hasChildrenRootWithItems() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + // Root of a non-empty list has children + QVERIFY(model.hasChildren(QModelIndex())); +} + +void ObjectListModelBaseTest::_hasChildrenNonRootAlwaysFalse() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + const QModelIndex idx = model.index(0, 0); + QVERIFY(idx.isValid()); + + // Non-root item should never have children in a flat list + QVERIFY(!model.hasChildren(idx)); +} + +UT_REGISTER_TEST(ObjectListModelBaseTest, TestLabel::Unit) + +#include "ObjectListModelBaseTest.moc" diff --git a/test/QmlControls/ObjectListModelBaseTest.h b/test/QmlControls/ObjectListModelBaseTest.h new file mode 100644 index 000000000000..0bcf270207e4 --- /dev/null +++ b/test/QmlControls/ObjectListModelBaseTest.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "UnitTest.h" + +class ObjectListModelBaseTest : public UnitTest +{ + Q_OBJECT + +private slots: + // index() overrides + void _indexValidRow(); + void _indexInvalidRow(); + void _indexNonZeroColumnInvalid(); + void _indexWithValidParentInvalid(); + + // parent() override + void _parentAlwaysInvalid(); + + // columnCount() override + void _columnCountAlwaysOne(); + + // hasChildren() override + void _hasChildrenRootEmpty(); + void _hasChildrenRootWithItems(); + void _hasChildrenNonRootAlwaysFalse(); +}; diff --git a/test/QmlControls/QmlObjectTreeModelTest.cc b/test/QmlControls/QmlObjectTreeModelTest.cc new file mode 100644 index 000000000000..9adf2d560a55 --- /dev/null +++ b/test/QmlControls/QmlObjectTreeModelTest.cc @@ -0,0 +1,685 @@ +#include "QmlObjectTreeModelTest.h" + +#include +#include +#include + +// --------------------------------------------------------------------------- +// DirtyObject — minimal QObject with a dirty property for signal testing +// --------------------------------------------------------------------------- +namespace { + +class DirtyObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) + +public: + explicit DirtyObject(QObject* parent = nullptr) : QObject(parent) {} + + bool dirty() const { return _dirty; } + void setDirty(bool dirty) + { + if (_dirty == dirty) return; + _dirty = dirty; + emit dirtyChanged(_dirty); + } + +signals: + void dirtyChanged(bool dirty); + +private: + bool _dirty = false; +}; + +} // namespace + +// =========================================================================== +// Construction & empty state +// =========================================================================== + +void QmlObjectTreeModelTest::_emptyModelDefaults() +{ + QmlObjectTreeModel model; + QCOMPARE(model.count(), 0); + QCOMPARE(model.rowCount(), 0); + QVERIFY(model.isEmpty()); + QCOMPARE(model.dirty(), false); +} + +void QmlObjectTreeModelTest::_roleNamesIncludeNodeType() +{ + QmlObjectTreeModel model; + const auto roles = model.roleNames(); + QVERIFY(roles.values().contains("object")); + QVERIFY(roles.values().contains("text")); + QVERIFY(roles.values().contains("nodeType")); +} + +// =========================================================================== +// Insert / Append +// =========================================================================== + +void QmlObjectTreeModelTest::_appendToRoot() +{ + QmlObjectTreeModel model; + QObject obj; + obj.setObjectName("item1"); + + const QModelIndex idx = model.appendItem(&obj); + QVERIFY(idx.isValid()); + QCOMPARE(idx.row(), 0); + QCOMPARE(model.count(), 1); + QCOMPARE(model.rowCount(), 1); + QCOMPARE(model.getObject(idx), &obj); +} + +void QmlObjectTreeModelTest::_appendWithNodeType() +{ + QmlObjectTreeModel model; + QObject obj; + + const QModelIndex idx = model.appendItem(&obj, QModelIndex(), QStringLiteral("myType")); + QVERIFY(idx.isValid()); + QCOMPARE(model.data(idx, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("myType")); +} + +void QmlObjectTreeModelTest::_insertAtPositions() +{ + QmlObjectTreeModel model; + QObject a, b, c; + a.setObjectName("a"); + b.setObjectName("b"); + c.setObjectName("c"); + + model.appendItem(&b); // [b] + model.insertItem(0, &a); // [a, b] + model.insertItem(2, &c); // [a, b, c] + + QCOMPARE(model.rowCount(), 3); + QCOMPARE(model.getObject(model.index(0, 0)), &a); + QCOMPARE(model.getObject(model.index(1, 0)), &b); + QCOMPARE(model.getObject(model.index(2, 0)), &c); +} + +void QmlObjectTreeModelTest::_appendToChild() +{ + QmlObjectTreeModel model; + QObject parent, child; + + const QModelIndex parentIdx = model.appendItem(&parent); + const QModelIndex childIdx = model.appendItem(&child, parentIdx); + + QVERIFY(childIdx.isValid()); + QCOMPARE(model.rowCount(parentIdx), 1); + QCOMPARE(model.getObject(childIdx), &child); + QCOMPARE(model.count(), 2); // parent + child +} + +void QmlObjectTreeModelTest::_insertInvalidParentLogsWarning() +{ + QmlObjectTreeModel model; + QObject obj; + + // Create a stale index from another model — will be invalid for this model + const QModelIndex badIndex = model.index(999, 0); + QVERIFY(!badIndex.isValid()); + + // appendItem with invalid index -> inserts at root (valid behavior) + const QModelIndex idx = model.appendItem(&obj, badIndex); + // Should still succeed since invalid parent = root + QVERIFY(idx.isValid()); +} + +void QmlObjectTreeModelTest::_insertOutOfRangeLogsWarning() +{ + QmlObjectTreeModel model; + QObject obj; + + const QModelIndex idx = model.insertItem(5, &obj); // row 5, but 0 children + QVERIFY(!idx.isValid()); // should fail gracefully + QCOMPARE(model.count(), 0); +} + +// =========================================================================== +// Remove +// =========================================================================== + +void QmlObjectTreeModelTest::_removeItemReturnsObject() +{ + QmlObjectTreeModel model; + QObject obj; + + const QModelIndex idx = model.appendItem(&obj); + QObject* removed = model.removeItem(idx); + + QCOMPARE(removed, &obj); + QCOMPARE(model.count(), 0); +} + +void QmlObjectTreeModelTest::_removeSubtreeDecrementsCount() +{ + QmlObjectTreeModel model; + QObject parentObj, child1, child2, grandchild; + + const QModelIndex parentIdx = model.appendItem(&parentObj); + const QModelIndex child1Idx = model.appendItem(&child1, parentIdx); + model.appendItem(&child2, parentIdx); + model.appendItem(&grandchild, child1Idx); + + QCOMPARE(model.count(), 4); + + // Remove parentObj — should remove parent + 2 children + 1 grandchild = 4 + model.removeItem(parentIdx); + QCOMPARE(model.count(), 0); +} + +void QmlObjectTreeModelTest::_removeAtConvenience() +{ + QmlObjectTreeModel model; + QObject a, b; + + model.appendItem(&a); + model.appendItem(&b); + + QObject* removed = model.removeAt(QModelIndex(), 0); + QCOMPARE(removed, &a); + QCOMPARE(model.count(), 1); +} + +void QmlObjectTreeModelTest::_removeChildrenKeepsParent() +{ + QmlObjectTreeModel model; + QObject parentObj, child1, child2; + + const QModelIndex parentIdx = model.appendItem(&parentObj); + model.appendItem(&child1, parentIdx); + model.appendItem(&child2, parentIdx); + + QCOMPARE(model.count(), 3); + model.removeChildren(parentIdx); + QCOMPARE(model.rowCount(parentIdx), 0); + QCOMPARE(model.count(), 1); // parent still there + QVERIFY(model.contains(&parentObj)); +} + +void QmlObjectTreeModelTest::_removeInvalidIndexReturnsNull() +{ + QmlObjectTreeModel model; + QObject* removed = model.removeItem(QModelIndex()); + QVERIFY(removed == nullptr); +} + +// =========================================================================== +// Count cache +// =========================================================================== + +void QmlObjectTreeModelTest::_countStaysConsistentAfterMixedOps() +{ + QmlObjectTreeModel model; + QObject objects[10]; + + // Insert 10 + for (int i = 0; i < 10; i++) { + model.appendItem(&objects[i]); + } + QCOMPARE(model.count(), 10); + + // Remove 5 (from end to avoid index shifting) + for (int i = 9; i >= 5; i--) { + model.removeAt(QModelIndex(), i); + } + QCOMPARE(model.count(), 5); + + // Add 3 more + QObject extras[3]; + for (int i = 0; i < 3; i++) { + model.appendItem(&extras[i]); + } + QCOMPARE(model.count(), 8); + + // Clear + model.clear(); + QCOMPARE(model.count(), 0); +} + +void QmlObjectTreeModelTest::_countReflectsNestedChildren() +{ + QmlObjectTreeModel model; + QObject root, c1, c2, gc1, gc2, gc3; + + const QModelIndex rootIdx = model.appendItem(&root); + const QModelIndex c1Idx = model.appendItem(&c1, rootIdx); + model.appendItem(&c2, rootIdx); + model.appendItem(&gc1, c1Idx); + model.appendItem(&gc2, c1Idx); + model.appendItem(&gc3, c1Idx); + + QCOMPARE(model.count(), 6); // root + 2 children + 3 grandchildren +} + +// =========================================================================== +// Clear +// =========================================================================== + +void QmlObjectTreeModelTest::_clearResetsToEmpty() +{ + QmlObjectTreeModel model; + QObject a, b, c; + model.appendItem(&a); + model.appendItem(&b); + model.appendItem(&c); + + model.clear(); + QCOMPARE(model.count(), 0); + QCOMPARE(model.rowCount(), 0); + QVERIFY(model.isEmpty()); +} + +void QmlObjectTreeModelTest::_clearAndDeleteContentsDeletesObjects() +{ + QmlObjectTreeModel model; + + auto* obj1 = new QObject; + auto* obj2 = new QObject; + + model.appendItem(obj1); + model.appendItem(obj2); + + QSignalSpy destroyed1(obj1, &QObject::destroyed); + QSignalSpy destroyed2(obj2, &QObject::destroyed); + + model.clearAndDeleteContents(); + QCOMPARE(model.count(), 0); + + // deleteLater requires the event loop to process deferred deletions + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + QCOMPARE(destroyed1.count(), 1); + QCOMPARE(destroyed2.count(), 1); +} + +void QmlObjectTreeModelTest::_clearOnEmptyIsNoop() +{ + QmlObjectTreeModel model; + QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset); + model.clear(); + QCOMPARE(resetSpy.count(), 0); // no-op — no reset emitted +} + +// =========================================================================== +// Tree navigation +// =========================================================================== + +void QmlObjectTreeModelTest::_parentOfRootChildIsInvalid() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + QVERIFY(!model.parent(idx).isValid()); +} + +void QmlObjectTreeModelTest::_parentOfNestedChild() +{ + QmlObjectTreeModel model; + QObject parentObj, childObj; + + const QModelIndex parentIdx = model.appendItem(&parentObj); + const QModelIndex childIdx = model.appendItem(&childObj, parentIdx); + + const QModelIndex retrievedParent = model.parent(childIdx); + QVERIFY(retrievedParent.isValid()); + QCOMPARE(model.getObject(retrievedParent), &parentObj); +} + +void QmlObjectTreeModelTest::_depthValues() +{ + QmlObjectTreeModel model; + QObject root, child, grandchild; + + const QModelIndex rootIdx = model.appendItem(&root); + const QModelIndex childIdx = model.appendItem(&child, rootIdx); + const QModelIndex gcIdx = model.appendItem(&grandchild, childIdx); + + QCOMPARE(model.depth(rootIdx), 0); + QCOMPARE(model.depth(childIdx), 1); + QCOMPARE(model.depth(gcIdx), 2); + QCOMPARE(model.depth(QModelIndex()), -1); // invalid +} + +void QmlObjectTreeModelTest::_indexForObjectFindsNested() +{ + QmlObjectTreeModel model; + QObject root, child, grandchild; + + const QModelIndex rootIdx = model.appendItem(&root); + const QModelIndex childIdx = model.appendItem(&child, rootIdx); + model.appendItem(&grandchild, childIdx); + + const QModelIndex found = model.indexForObject(&grandchild); + QVERIFY(found.isValid()); + QCOMPARE(model.getObject(found), &grandchild); +} + +void QmlObjectTreeModelTest::_indexForObjectNotFound() +{ + QmlObjectTreeModel model; + QObject notInModel; + QVERIFY(!model.indexForObject(¬InModel).isValid()); + QVERIFY(!model.indexForObject(nullptr).isValid()); +} + +void QmlObjectTreeModelTest::_containsWorks() +{ + QmlObjectTreeModel model; + QObject inModel, notInModel; + model.appendItem(&inModel); + + QVERIFY(model.contains(&inModel)); + QVERIFY(!model.contains(¬InModel)); +} + +void QmlObjectTreeModelTest::_columnCountAlwaysOne() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + + QCOMPARE(model.columnCount(), 1); + QCOMPARE(model.columnCount(idx), 1); +} + +void QmlObjectTreeModelTest::_hasChildrenLeafVsParent() +{ + QmlObjectTreeModel model; + QObject parentObj, childObj; + + const QModelIndex parentIdx = model.appendItem(&parentObj); + QVERIFY(!model.hasChildren(parentIdx)); // no children yet + + const QModelIndex childIdx = model.appendItem(&childObj, parentIdx); + QVERIFY(model.hasChildren(parentIdx)); // now has children + QVERIFY(!model.hasChildren(childIdx)); // leaf + QVERIFY(model.hasChildren()); // root has children +} + +// =========================================================================== +// Data & SetData +// =========================================================================== + +void QmlObjectTreeModelTest::_dataObjectRole() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + + const QVariant val = model.data(idx, Qt::UserRole); // ObjectRole + QCOMPARE(val.value(), &obj); +} + +void QmlObjectTreeModelTest::_dataTextRole() +{ + QmlObjectTreeModel model; + QObject obj; + obj.setObjectName("hello"); + const QModelIndex idx = model.appendItem(&obj); + + QCOMPARE(model.data(idx, Qt::UserRole + 1).toString(), QStringLiteral("hello")); // TextRole +} + +void QmlObjectTreeModelTest::_dataNodeTypeRole() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj, QModelIndex(), QStringLiteral("missionItem")); + + QCOMPARE(model.data(idx, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("missionItem")); +} + +void QmlObjectTreeModelTest::_dataInvalidIndexReturnsEmpty() +{ + QmlObjectTreeModel model; + QVERIFY(!model.data(QModelIndex(), Qt::UserRole).isValid()); +} + +void QmlObjectTreeModelTest::_setDataReplacesObject() +{ + QmlObjectTreeModel model; + QObject obj1, obj2; + + const QModelIndex idx = model.appendItem(&obj1); + QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged); + + bool ok = model.setData(idx, QVariant::fromValue(&obj2), Qt::UserRole); + QVERIFY(ok); + QCOMPARE(model.getObject(idx), &obj2); + QCOMPARE(dataChangedSpy.count(), 1); +} + +void QmlObjectTreeModelTest::_setDataNonObjectRoleReturnsFalse() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + + QVERIFY(!model.setData(idx, QVariant("test"), Qt::DisplayRole)); +} + +// =========================================================================== +// Dirty tracking +// =========================================================================== + +void QmlObjectTreeModelTest::_insertSetsDirty() +{ + QmlObjectTreeModel model; + model.setDirty(false); + + QObject obj; + model.appendItem(&obj); + QVERIFY(model.dirty()); +} + +void QmlObjectTreeModelTest::_childDirtyPropagates() +{ + QmlObjectTreeModel model; + DirtyObject obj; + + QSignalSpy dirtySpy(&model, &QmlObjectTreeModel::dirtyChanged); + QVERIFY(dirtySpy.isValid()); + + model.appendItem(&obj); + model.setDirty(false); + dirtySpy.clear(); + + obj.setDirty(true); + QCOMPARE(dirtySpy.count(), 1); + QCOMPARE(dirtySpy.takeFirst().at(0).toBool(), true); + QVERIFY(model.dirty()); +} + +void QmlObjectTreeModelTest::_removeDisconnectsDirty() +{ + QmlObjectTreeModel model; + DirtyObject obj; + + model.appendItem(&obj); + const QModelIndex idx = model.indexForObject(&obj); + model.removeItem(idx); + model.setDirty(false); + + QSignalSpy dirtySpy(&model, &QmlObjectTreeModel::dirtyChanged); + obj.setDirty(true); + + QCOMPARE(dirtySpy.count(), 0); + QVERIFY(!model.dirty()); +} + +// =========================================================================== +// Model signals +// =========================================================================== + +void QmlObjectTreeModelTest::_insertEmitsRowsInserted() +{ + QmlObjectTreeModel model; + QSignalSpy spy(&model, &QAbstractItemModel::rowsInserted); + + QObject obj; + model.appendItem(&obj); + + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(1).toInt(), 0); // first = 0 + QCOMPARE(spy.at(0).at(2).toInt(), 0); // last = 0 +} + +void QmlObjectTreeModelTest::_removeEmitsRowsRemoved() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + + QSignalSpy spy(&model, &QAbstractItemModel::rowsRemoved); + model.removeItem(idx); + + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(1).toInt(), 0); // first = 0 + QCOMPARE(spy.at(0).at(2).toInt(), 0); // last = 0 +} + +void QmlObjectTreeModelTest::_clearEmitsModelReset() +{ + QmlObjectTreeModel model; + QObject obj; + model.appendItem(&obj); + + QSignalSpy spy(&model, &QAbstractItemModel::modelReset); + model.clear(); + + QCOMPARE(spy.count(), 1); +} + +void QmlObjectTreeModelTest::_countChangedEmitted() +{ + QmlObjectTreeModel model; + QSignalSpy spy(&model, &QmlObjectTreeModel::countChanged); + + QObject obj; + model.appendItem(&obj); + QVERIFY(spy.count() >= 1); + + spy.clear(); + model.removeItem(model.index(0, 0)); + QVERIFY(spy.count() >= 1); +} + +// =========================================================================== +// Nested reset +// =========================================================================== + +void QmlObjectTreeModelTest::_nestedResetOnlyEmitsOnce() +{ + QmlObjectTreeModel model; + QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset); + + model.beginResetModel(); + model.beginResetModel(); // nested — should NOT emit again + + QObject a, b; + model.appendItem(&a); + model.appendItem(&b); + + model.endResetModel(); // inner — no-op + model.endResetModel(); // outer — actually emits + + QCOMPARE(resetSpy.count(), 1); +} + +void QmlObjectTreeModelTest::_insertDuringResetNoSignals() +{ + QmlObjectTreeModel model; + model.beginResetModel(); + + QSignalSpy insertSpy(&model, &QAbstractItemModel::rowsInserted); + QSignalSpy countSpy(&model, &QmlObjectTreeModel::countChanged); + + QObject obj; + model.appendItem(&obj); + + QCOMPARE(insertSpy.count(), 0); // suppressed during reset + QCOMPARE(countSpy.count(), 0); // suppressed during reset + + model.endResetModel(); + QCOMPARE(model.count(), 1); // but count is correct +} + +// =========================================================================== +// insertRows / removeRows guard +// =========================================================================== + +void QmlObjectTreeModelTest::_insertRowsReturnsFalse() +{ + QmlObjectTreeModel model; + QVERIFY(!model.insertRows(0, 1)); +} + +void QmlObjectTreeModelTest::_removeRowsReturnsFalse() +{ + QmlObjectTreeModel model; + QVERIFY(!model.removeRows(0, 1)); +} + +// =========================================================================== +// QPersistentModelIndex stability +// =========================================================================== + +void QmlObjectTreeModelTest::_persistentIndexSurvivesInsertBefore() +{ + QmlObjectTreeModel model; + QObject a, b; + + model.appendItem(&a); + QPersistentModelIndex persistentIdx(model.index(0, 0)); + QCOMPARE(model.getObject(persistentIdx), &a); + + // Insert before — persistent index should shift to row 1 + model.insertItem(0, &b); + QVERIFY(persistentIdx.isValid()); + QCOMPARE(persistentIdx.row(), 1); + QCOMPARE(model.getObject(persistentIdx), &a); +} + +void QmlObjectTreeModelTest::_persistentIndexSurvivesRemoveOther() +{ + QmlObjectTreeModel model; + QObject a, b, c; + + model.appendItem(&a); + model.appendItem(&b); + model.appendItem(&c); + + QPersistentModelIndex persistentIdx(model.index(2, 0)); // points to c + QCOMPARE(model.getObject(persistentIdx), &c); + + // Remove a (row 0) — persistent should shift to row 1 + model.removeAt(QModelIndex(), 0); + QVERIFY(persistentIdx.isValid()); + QCOMPARE(persistentIdx.row(), 1); + QCOMPARE(model.getObject(persistentIdx), &c); +} + +// =========================================================================== +// Null object edge case +// =========================================================================== + +void QmlObjectTreeModelTest::_insertNullObject() +{ + QmlObjectTreeModel model; + const QModelIndex idx = model.appendItem(nullptr); + + QVERIFY(idx.isValid()); + QCOMPARE(model.count(), 1); + QVERIFY(model.getObject(idx) == nullptr); + QVERIFY(!model.data(idx, Qt::UserRole).isValid()); // null object → invalid variant +} + +UT_REGISTER_TEST(QmlObjectTreeModelTest, TestLabel::Unit) + +#include "QmlObjectTreeModelTest.moc" diff --git a/test/QmlControls/QmlObjectTreeModelTest.h b/test/QmlControls/QmlObjectTreeModelTest.h new file mode 100644 index 000000000000..83fcd5aa9f28 --- /dev/null +++ b/test/QmlControls/QmlObjectTreeModelTest.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include "QmlObjectTreeModel.h" +#include "UnitTest.h" + +class QmlObjectTreeModelTest : public UnitTest +{ + Q_OBJECT + +private slots: + // Construction & empty state + void _emptyModelDefaults(); + void _roleNamesIncludeNodeType(); + + // Insert / Append + void _appendToRoot(); + void _appendWithNodeType(); + void _insertAtPositions(); + void _appendToChild(); + void _insertInvalidParentLogsWarning(); + void _insertOutOfRangeLogsWarning(); + + // Remove + void _removeItemReturnsObject(); + void _removeSubtreeDecrementsCount(); + void _removeAtConvenience(); + void _removeChildrenKeepsParent(); + void _removeInvalidIndexReturnsNull(); + + // Count cache + void _countStaysConsistentAfterMixedOps(); + void _countReflectsNestedChildren(); + + // Clear + void _clearResetsToEmpty(); + void _clearAndDeleteContentsDeletesObjects(); + void _clearOnEmptyIsNoop(); + + // Tree navigation + void _parentOfRootChildIsInvalid(); + void _parentOfNestedChild(); + void _depthValues(); + void _indexForObjectFindsNested(); + void _indexForObjectNotFound(); + void _containsWorks(); + void _columnCountAlwaysOne(); + void _hasChildrenLeafVsParent(); + + // Data & SetData + void _dataObjectRole(); + void _dataTextRole(); + void _dataNodeTypeRole(); + void _dataInvalidIndexReturnsEmpty(); + void _setDataReplacesObject(); + void _setDataNonObjectRoleReturnsFalse(); + + // Dirty tracking + void _insertSetsDirty(); + void _childDirtyPropagates(); + void _removeDisconnectsDirty(); + + // Model signals + void _insertEmitsRowsInserted(); + void _removeEmitsRowsRemoved(); + void _clearEmitsModelReset(); + void _countChangedEmitted(); + + // Nested reset + void _nestedResetOnlyEmitsOnce(); + void _insertDuringResetNoSignals(); + + // insertRows / removeRows guard + void _insertRowsReturnsFalse(); + void _removeRowsReturnsFalse(); + + // QPersistentModelIndex stability + void _persistentIndexSurvivesInsertBefore(); + void _persistentIndexSurvivesRemoveOther(); + + // Null object edge case + void _insertNullObject(); +}; From 0f30c72db34c149df93a07e463e7c4cf8eb84400 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 5 Mar 2026 10:02:05 -0800 Subject: [PATCH 031/119] Fix MAVLink message bounds validation vulnerabilities Harden MAVLink message handlers against malicious or malformed payloads that could trigger out-of-bounds memory access. ImageProtocolManager (GHSA-v5rc-wh3c-c4cw): - Validate DATA_TRANSMISSION_HANDSHAKE fields (size, payload, packets) before allocating the image buffer - Enforce 1 MB upper bound on image size - Reject payload values exceeding ENCAPSULATED_DATA data[253] array size - Pre-allocate image buffer to declared size instead of growing via unchecked indexed writes - Replace byte-by-byte copy loop with bounds-clamped memcpy - Cast seqnr to uint32_t before multiplication to prevent overflow Vehicle (LOG_DATA): - Add bounds check on log.count against sizeof(log.data) before emitting the signal, preventing downstream consumers from reading past the 90-byte data array FTPManager (FILE_TRANSFER_PROTOCOL): - Validate hdr.size against sizeof(request->data) at the single message entry point, protecting all downstream handlers (burst read, list directory, fill missing blocks) from reading past the 239-byte data array Fixes: GHSA-v5rc-wh3c-c4cw --- src/MAVLink/ImageProtocolManager.cc | 34 ++++++++++++++++++++++++----- src/Vehicle/FTPManager.cc | 6 +++++ src/Vehicle/Vehicle.cc | 8 +++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/MAVLink/ImageProtocolManager.cc b/src/MAVLink/ImageProtocolManager.cc index fb60b80bb674..593e2e84ff72 100644 --- a/src/MAVLink/ImageProtocolManager.cc +++ b/src/MAVLink/ImageProtocolManager.cc @@ -1,6 +1,8 @@ #include "ImageProtocolManager.h" #include "QGCLoggingCategory.h" +#include + QGC_LOGGING_CATEGORY(ImageProtocolManagerLog, "MAVLink.ImageProtocolManager") ImageProtocolManager::ImageProtocolManager(QObject *parent) @@ -49,6 +51,27 @@ void ImageProtocolManager::mavlinkMessageReceived(const mavlink_message_t &messa _imageBytes.clear(); mavlink_msg_data_transmission_handshake_decode(&message, &_imageHandshake); qCDebug(ImageProtocolManagerLog) << QStringLiteral("DATA_TRANSMISSION_HANDSHAKE: type(%1) width(%2) height (%3)").arg(_imageHandshake.type).arg(_imageHandshake.width).arg(_imageHandshake.height); + + // Validate handshake fields to prevent out-of-bounds writes on subsequent ENCAPSULATED_DATA + static constexpr uint32_t kMaxImageSize = 1u * 1024u * 1024u; // 1 MB upper bound (optical flow images are typically small grayscale frames) + if (_imageHandshake.size == 0 || _imageHandshake.payload == 0 || _imageHandshake.packets == 0) { + qCWarning(ImageProtocolManagerLog) << "DATA_TRANSMISSION_HANDSHAKE: Invalid field(s) - size:" << _imageHandshake.size + << "payload:" << _imageHandshake.payload << "packets:" << _imageHandshake.packets; + _imageHandshake = {}; + break; + } + if (_imageHandshake.size > kMaxImageSize) { + qCWarning(ImageProtocolManagerLog) << "DATA_TRANSMISSION_HANDSHAKE: Image size exceeds limit. size:" << _imageHandshake.size; + _imageHandshake = {}; + break; + } + if (_imageHandshake.payload > sizeof(mavlink_encapsulated_data_t::data)) { + qCWarning(ImageProtocolManagerLog) << "DATA_TRANSMISSION_HANDSHAKE: payload exceeds ENCAPSULATED_DATA data field size. payload:" << _imageHandshake.payload; + _imageHandshake = {}; + break; + } + + _imageBytes.resize(_imageHandshake.size, '\0'); break; } case MAVLINK_MSG_ID_ENCAPSULATED_DATA: @@ -61,16 +84,17 @@ void ImageProtocolManager::mavlinkMessageReceived(const mavlink_message_t &messa mavlink_encapsulated_data_t encapsulatedData; mavlink_msg_encapsulated_data_decode(&message, &encapsulatedData); - uint32_t bytePosition = encapsulatedData.seqnr * _imageHandshake.payload; + const uint32_t bytePosition = static_cast(encapsulatedData.seqnr) * _imageHandshake.payload; if (bytePosition >= _imageHandshake.size) { qCWarning(ImageProtocolManagerLog) << "ENCAPSULATED_DATA: seqnr is past end of image size. seqnr:" << encapsulatedData.seqnr << "_imageHandshake.size:" << _imageHandshake.size; break; } - for (uint8_t i = 0; i < _imageHandshake.payload; i++) { - _imageBytes[bytePosition] = encapsulatedData.data[i]; - bytePosition++; - } + // Clamp the number of bytes to copy so we never write past the declared image size + const uint32_t bytesRemaining = _imageHandshake.size - bytePosition; + const uint32_t bytesToCopy = qMin(static_cast(_imageHandshake.payload), bytesRemaining); + + (void) memcpy(_imageBytes.data() + bytePosition, encapsulatedData.data, bytesToCopy); // We use the packets field to track completion _imageHandshake.packets--; diff --git a/src/Vehicle/FTPManager.cc b/src/Vehicle/FTPManager.cc index 328db8d558c4..935f59646177 100644 --- a/src/Vehicle/FTPManager.cc +++ b/src/Vehicle/FTPManager.cc @@ -663,6 +663,12 @@ void FTPManager::_mavlinkMessageReceived(const mavlink_message_t& message) MavlinkFTP::Request* request = (MavlinkFTP::Request*)&data.payload[0]; + // Reject messages where hdr.size exceeds data array bounds to prevent over-reads + if (request->hdr.size > sizeof(request->data)) { + qCWarning(FTPManagerLog) << "_mavlinkMessageReceived: hdr.size exceeds data array, discarding." << request->hdr.size; + return; + } + // Ignore old/reordered packets (handle wrap-around properly) uint16_t actualIncomingSeqNumber = request->hdr.seqNumber; if ((uint16_t)((_expectedIncomingSeqNumber - 1) - actualIncomingSeqNumber) < (std::numeric_limits::max()/2)) { diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index 23e896b8b82a..2b531563eafd 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -605,7 +605,7 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes mavlink_serial_control_t ser; mavlink_msg_serial_control_decode(&message, &ser); if (static_cast(ser.count) > sizeof(ser.data)) { - qWarning() << "Invalid count for SERIAL_CONTROL, discarding." << ser.count; + qCWarning(VehicleLog) << "Invalid count for SERIAL_CONTROL, discarding." << ser.count; } else { emit mavlinkSerialControl(ser.device, ser.flags, ser.timeout, ser.baudrate, QByteArray(reinterpret_cast(ser.data), ser.count)); @@ -643,7 +643,11 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes { mavlink_log_data_t log{}; mavlink_msg_log_data_decode(&message, &log); - emit logData(log.ofs, log.id, log.count, log.data); + if (static_cast(log.count) > sizeof(log.data)) { + qCWarning(VehicleLog) << "Invalid count for LOG_DATA, discarding." << log.count; + } else { + emit logData(log.ofs, log.id, log.count, log.data); + } break; } case MAVLINK_MSG_ID_MESSAGE_INTERVAL: From def99b992acc086464d03793fdada8f3c1146be6 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 6 Mar 2026 09:58:28 -0800 Subject: [PATCH 032/119] Docs: clarify logging level guidelines in CODING_STYLE.md Document the intended use of qCDebug, qCWarning, and qCCritical: - qCDebug for general logging (only displays when enabled) - qCWarning for handled-but-unusual error flows - qCCritical for coding errors (causes unit test failures) - Explicitly forbid uncategorized qDebug() --- CODING_STYLE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CODING_STYLE.md b/CODING_STYLE.md index c86e0b9936d1..573c752363fa 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -130,8 +130,24 @@ Q_DECLARE_LOGGING_CATEGORY(MyComponentLog) QGC_LOGGING_CATEGORY(MyComponentLog, "qgc.component.name") // Use categorized logging + +// qCDebug is used for general logging. These logs only display when turned on. +// Do not use qCInfo, always use qCDebug. qCDebug(MyComponentLog) << "Debug message"; + +// qCWarning is used for logging of error flows which are handled but unusual. +// For example the vehicle failed to respond to a request. +// These logs will display even when the category is not enabled to display. qCWarning(MyComponentLog) << "Warning message"; + +// qCCritical is used to indicate a coding error. +// Example: An internal Fact is using an unsupported Fact type. +// These logs will cause unit tests to fail if they are hit. +// These logs will display even when the category is not enabled to display. +qCCritical(MyComponentLog) << "Internal Error: ..."; + +// Never use uncategorized logging +qDebug() << "..."; // This is wrong ``` ## Qt6 / QML Integration From ecaef664d6028d96cd8305a9510697eb9e3df867 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 6 Mar 2026 10:00:01 -0800 Subject: [PATCH 033/119] Plan: add Plan File and Defaults tree nodes, slim MissionSettingsEditor Restructure the PlanView tree to include five top-level groups: Plan File - editable file name, save-state indicators Defaults - altitude mode, flight speed, vehicle info, launch position Mission - mission items (as before) GeoFence - fence editor Rally - rally point editor The Defaults node absorbs most of what was in MissionSettingsEditor, which is now reduced to only the Initial Camera Settings section. MissionController changes: - Add _planFileGroupNode, _defaultsGroupNode and their marker children - Expose public static constexpr tree index constants - Wire up persistent model indexes for all five groups MissionItemTreeView.qml: - Add planFileInfoComponent delegate with editable file name TextField - Add defaultsEditorComponent with altitude mode, speed, vehicle info, and launch position controls (moved from MissionSettingsEditor) - Add Connections handler for firmwareTypeChanged to fall back from TerrainFrame when the vehicle no longer supports it - Remove dead _isActiveGroup property from groupHeaderComponent - Fix mismatched comment above defaultsEditorComponent MissionSettingsItem.h: - Remove unused missionVehicle() getter --- src/MissionManager/MissionController.cc | 16 + src/MissionManager/MissionController.h | 14 + src/MissionManager/MissionSettingsItem.h | 4 +- src/QmlControls/MissionItemTreeView.qml | 367 ++++++++++++++++++++-- src/QmlControls/MissionSettingsEditor.qml | 317 +------------------ 5 files changed, 372 insertions(+), 346 deletions(-) diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index c9d856e506bc..61a506fcfa60 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -1819,6 +1819,22 @@ void MissionController::_setupTreeModel(void) // We use incremental inserts so QPersistentModelIndex stays valid. _visualItemsTree.clear(); + // ── Plan File group ── + _planFileGroupNode.setObjectName(tr("Plan File")); + _planFileGroupIndex = QPersistentModelIndex( + _visualItemsTree.appendItem(&_planFileGroupNode, QModelIndex(), QStringLiteral("planFileGroup"))); + + _planFileInfoMarker.setObjectName(QStringLiteral("planFileInfo")); + _visualItemsTree.appendItem(&_planFileInfoMarker, _planFileGroupIndex, QStringLiteral("planFileInfo")); + + // ── Defaults group ── + _defaultsGroupNode.setObjectName(tr("Defaults")); + _defaultsGroupIndex = QPersistentModelIndex( + _visualItemsTree.appendItem(&_defaultsGroupNode, QModelIndex(), QStringLiteral("defaultsGroup"))); + + _defaultsInfoMarker.setObjectName(QStringLiteral("defaultsInfo")); + _visualItemsTree.appendItem(&_defaultsInfoMarker, _defaultsGroupIndex, QStringLiteral("defaultsInfo")); + // ── Mission Items group ── _missionItemsGroupNode.setObjectName(tr("Mission Items")); _missionGroupIndex = QPersistentModelIndex( diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 07bbfa61867a..3b7898d9f779 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -267,6 +267,14 @@ class MissionController : public PlanElementController QGroundControlQmlGlobal::AltMode globalAltitudeModeDefault(void); void setGlobalAltitudeMode(QGroundControlQmlGlobal::AltMode altMode); + // Top-level group row indices in _visualItemsTree (must match _setupTreeModel order) + static constexpr int kPlanFileGroupRow = 0; + static constexpr int kDefaultsGroupRow = 1; + static constexpr int kMissionGroupRow = 2; + static constexpr int kFenceGroupRow = 3; + static constexpr int kRallyGroupRow = 4; + static constexpr int kGroupCount = 5; + signals: void visualItemsChanged (void); void splitSegmentChanged (void); @@ -385,9 +393,15 @@ private slots: int _missionItemCount = 0; QmlObjectListModel* _visualItems = nullptr; QmlObjectTreeModel _visualItemsTree; + QPersistentModelIndex _planFileGroupIndex; ///< Persistent index for "Plan File" group in tree + QPersistentModelIndex _defaultsGroupIndex; ///< Persistent index for "Defaults" group in tree QPersistentModelIndex _missionGroupIndex; ///< Persistent index for "Mission Items" group in tree QPersistentModelIndex _fenceGroupIndex; ///< Persistent index for "GeoFence" group in tree QPersistentModelIndex _rallyGroupIndex; ///< Persistent index for "Rally Points" group in tree + QObject _planFileGroupNode; ///< Group node for "Plan File" in tree view + QObject _planFileInfoMarker; ///< Marker child for plan file info delegate + QObject _defaultsGroupNode; ///< Group node for "Defaults" in tree view + QObject _defaultsInfoMarker; ///< Marker child for defaults editor delegate QObject _missionItemsGroupNode; ///< Group node for "Mission Items" in tree view QObject _fenceGroupNode; ///< Group node for "GeoFence" in tree view QObject _rallyGroupNode; ///< Group node for "Rally Points" in tree view diff --git a/src/MissionManager/MissionSettingsItem.h b/src/MissionManager/MissionSettingsItem.h index d03adbca1205..61c71ab6e298 100644 --- a/src/MissionManager/MissionSettingsItem.h +++ b/src/MissionManager/MissionSettingsItem.h @@ -63,8 +63,8 @@ class MissionSettingsItem : public ComplexMissionItem bool isStandaloneCoordinate (void) const final { return false; } bool specifiesCoordinate (void) const final; bool specifiesAltitudeOnly (void) const final { return false; } - QString commandDescription (void) const final { return tr("Mission Settings"); } - QString commandName (void) const final { return tr("Mission Settings"); } + QString commandDescription (void) const final { return tr("Initial Camera Settings"); } + QString commandName (void) const final { return tr("Initial Camera Settings"); } QString abbreviation (void) const final; QGeoCoordinate coordinate (void) const final { return _plannedHomePositionCoordinate; } // Includes altitude QGeoCoordinate exitCoordinate (void) const final { return _plannedHomePositionCoordinate; } diff --git a/src/QmlControls/MissionItemTreeView.qml b/src/QmlControls/MissionItemTreeView.qml index f77a66149880..370c4a58331e 100644 --- a/src/QmlControls/MissionItemTreeView.qml +++ b/src/QmlControls/MissionItemTreeView.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls +import QGroundControl.FactControls /// Unified plan tree view showing Mission Items, GeoFence, and Rally Points /// as collapsible sections using a real TreeView with type-discriminating delegates. @@ -33,9 +34,17 @@ TreeView { QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Horizontal } QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Vertical } + // After full collapse, top-level group rows are: + // 0 = Plan File, 1 = Defaults, 2 = Mission Items, 3 = GeoFence, 4 = Rally Points + readonly property int _rowPlanFile: 0 + readonly property int _rowDefaults: 1 + readonly property int _rowMission: 2 + readonly property int _rowFence: 3 + readonly property int _rowRally: 4 + Component.onCompleted: { // Expand only Mission Items by default - root.expand(0) + root.expand(_rowMission) } Connections { @@ -43,7 +52,7 @@ TreeView { function onVisualItemsChanged() { // Mission group always expanded after rebuild (clear / load) root.collapseRecursively() - root.expand(0) + root.expand(_rowMission) _editingLayer = _layerMission } } @@ -51,10 +60,28 @@ TreeView { // Switching editing layer on group expand — exclusive: only one group expanded at a time. // Clicking an already-expanded group collapses it (toggle behavior). function _expandExclusive(clickedNodeType) { - // Check if the clicked group is already active - var alreadyActive = (clickedNodeType === "missionGroup" && _editingLayer === _layerMission) || - (clickedNodeType === "fenceGroup" && _editingLayer === _layerFence) || - (clickedNodeType === "rallyGroup" && _editingLayer === _layerRally) + // Check if the clicked group is already expanded + let alreadyActive = false + switch (clickedNodeType) { + case "planFileGroup": + alreadyActive = root.isExpanded(_rowPlanFile) + break + case "defaultsGroup": + alreadyActive = root.isExpanded(_rowDefaults) + break + case "missionGroup": + alreadyActive = _editingLayer === _layerMission + break + case "fenceGroup": + alreadyActive = _editingLayer === _layerFence + break + case "rallyGroup": + alreadyActive = _editingLayer === _layerRally + break + default: + alreadyActive = false + break + } // Collapse everything root.collapseRecursively() @@ -68,17 +95,28 @@ TreeView { } // Determine target visual row and editing layer from nodeType - // After full collapse, groups are always at rows 0, 1, 2 - var targetRow = -1 - if (clickedNodeType === "missionGroup") { - targetRow = 0 + let targetRow = -1 + switch (clickedNodeType) { + case "planFileGroup": + targetRow = _rowPlanFile + _editingLayer = -1 + break + case "defaultsGroup": + targetRow = _rowDefaults + _editingLayer = -1 + break + case "missionGroup": + targetRow = _rowMission _editingLayer = _layerMission - } else if (clickedNodeType === "fenceGroup") { - targetRow = 1 + break + case "fenceGroup": + targetRow = _rowFence _editingLayer = _layerFence - } else if (clickedNodeType === "rallyGroup") { - targetRow = 2 + break + case "rallyGroup": + targetRow = _rowRally _editingLayer = _layerRally + break } if (targetRow >= 0) { @@ -129,9 +167,13 @@ TreeView { // destroyed, which would cause "Cannot read property of null" // warnings in every downstream editor binding. switch (delegateRoot.nodeType) { + case "planFileGroup": return groupHeaderComponent + case "defaultsGroup": return groupHeaderComponent case "missionGroup": return groupHeaderComponent case "fenceGroup": return groupHeaderComponent case "rallyGroup": return groupHeaderComponent + case "planFileInfo": return planFileInfoComponent + case "defaultsInfo": return defaultsEditorComponent case "missionItem": return delegateRoot.nodeObject ? missionItemComponent : null case "fenceEditor": return delegateRoot.nodeObject ? fenceEditorComponent : null case "rallyHeader": return delegateRoot.nodeObject ? rallyHeaderComponent : null @@ -147,35 +189,28 @@ TreeView { Rectangle { width: delegateRoot.width - height: groupHeaderRow.height + ScreenTools.defaultFontPixelHeight * 0.5 - color: _isActiveGroup ? qgcPal.buttonHighlight : qgcPal.windowShade - - property bool _isActiveGroup: { - if (delegateRoot.nodeType === "missionGroup") return _editingLayer === _layerMission - if (delegateRoot.nodeType === "fenceGroup") return _editingLayer === _layerFence - if (delegateRoot.nodeType === "rallyGroup") return _editingLayer === _layerRally - return false - } + height: ScreenTools.implicitComboBoxHeight + ScreenTools.defaultFontPixelWidth + color: qgcPal.windowShade Row { id: groupHeaderRow spacing: ScreenTools.defaultFontPixelWidth * 0.5 anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: ScreenTools.defaultFontPixelWidth * 0.5 - - QGCLabel { - text: delegateRoot.expanded ? "\u25BE" : "\u25B8" - color: parent.parent._isActiveGroup ? qgcPal.buttonHighlightText : qgcPal.text - font.pointSize: ScreenTools.defaultFontPointSize + anchors.left: parent.left + anchors.leftMargin: ScreenTools.defaultFontPixelWidth * 0.5 + + QGCColoredImage { + width: ScreenTools.defaultFontPixelHeight * 0.75 + height: width + source: "/InstrumentValueIcons/cheveron-right.svg" + color: qgcPal.text anchors.verticalCenter: parent.verticalCenter + rotation: delegateRoot.expanded ? 90 : 0 } QGCLabel { text: delegateRoot.nodeObject ? delegateRoot.nodeObject.objectName : "" - font.pointSize: ScreenTools.mediumFontPointSize font.bold: true - color: parent.parent._isActiveGroup ? qgcPal.buttonHighlightText : qgcPal.text anchors.verticalCenter: parent.verticalCenter } } @@ -187,6 +222,274 @@ TreeView { } } + // ── Plan file info delegate ── + Component { + id: planFileInfoComponent + + Rectangle { + width: delegateRoot.width + height: planFileColumn.height + ScreenTools.defaultFontPixelHeight + color: qgcPal.windowShadeDark + + Column { + id: planFileColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: ScreenTools.defaultFontPixelWidth + spacing: ScreenTools.defaultFontPixelHeight * 0.25 + + QGCTextField { + id: planNameField + placeholderText: qsTr("Untitled") + width: parent.width + + Component.onCompleted: text = root.planMasterController.currentPlanFileName + + Connections { + target: root.planMasterController + function onCurrentPlanFileNameChanged() { + if (!planNameField.activeFocus) { + planNameField.text = root.planMasterController.currentPlanFileName + } + } + } + + onEditingFinished: root.planMasterController.currentPlanFileName = text + } + } + } + } + + // ── Defaults editor delegate ── + Component { + id: defaultsEditorComponent + + Rectangle { + id: defaultsRect + width: delegateRoot.width + height: defaultsColumn.height + ScreenTools.defaultFontPixelHeight + color: qgcPal.windowShadeDark + + property var _missionController: root._missionController + property var _controllerVehicle: root.planMasterController.controllerVehicle + property var _visualItems: root._missionController.visualItems + property bool _noMissionItemsAdded: _visualItems ? _visualItems.count <= 1 : true + property var _settingsItem: _visualItems && _visualItems.count > 0 ? _visualItems.get(0) : null + property bool _multipleFirmware: !QGroundControl.singleFirmwareSupport + property bool _multipleVehicleTypes: !QGroundControl.singleVehicleSupport + property bool _allowFWVehicleTypeSelection: _noMissionItemsAdded && !globals.activeVehicle + property bool _showCruiseSpeed: _controllerVehicle ? !_controllerVehicle.multiRotor : false + property bool _showHoverSpeed: _controllerVehicle ? (_controllerVehicle.multiRotor || _controllerVehicle.vtol) : false + property bool _vehicleHasHomePosition: _controllerVehicle ? _controllerVehicle.homePosition.isValid : false + property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly + property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 16 + readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2 + + Connections { + target: defaultsRect._controllerVehicle + function onFirmwareTypeChanged() { + if (!defaultsRect._controllerVehicle.supportsTerrainFrame + && defaultsRect._missionController.globalAltitudeMode === QGroundControl.AltitudeModeTerrainFrame) { + defaultsRect._missionController.globalAltitudeMode = QGroundControl.AltitudeModeCalcAboveTerrain + } + } + } + + Component { id: altModeDialogComponent; AltModeDialog { } } + + QGCPopupDialogFactory { + id: defaultsAltModeDialogFactory + dialogComponent: altModeDialogComponent + } + + ColumnLayout { + id: defaultsColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: ScreenTools.defaultFontPixelWidth + spacing: ScreenTools.defaultFontPixelHeight * 0.5 + + LabelledButton { + Layout.fillWidth: true + label: qsTr("Altitude Mode") + buttonText: QGroundControl.altitudeModeShortDescription(defaultsRect._missionController.globalAltitudeMode) + + onClicked: { + let removeModes = [] + let updateFunction = function(altMode) { defaultsRect._missionController.globalAltitudeMode = altMode } + if (!defaultsRect._controllerVehicle.supportsTerrainFrame) { + removeModes.push(QGroundControl.AltitudeModeTerrainFrame) + } + if (!defaultsRect._noMissionItemsAdded) { + if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeRelative) { + removeModes.push(QGroundControl.AltitudeModeRelative) + } + if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeAbsolute) { + removeModes.push(QGroundControl.AltitudeModeAbsolute) + } + if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeCalcAboveTerrain) { + removeModes.push(QGroundControl.AltitudeModeCalcAboveTerrain) + } + if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeTerrainFrame) { + removeModes.push(QGroundControl.AltitudeModeTerrainFrame) + } + } + defaultsAltModeDialogFactory.open({ rgRemoveModes: removeModes, updateAltModeFn: updateFunction }) + } + } + + FactTextFieldSlider { + Layout.fillWidth: true + label: qsTr("Waypoints Altitude") + fact: QGroundControl.settingsManager.appSettings.defaultMissionItemAltitude + } + + FactTextFieldSlider { + Layout.fillWidth: true + label: qsTr("Flight Speed") + fact: defaultsRect._settingsItem ? defaultsRect._settingsItem.speedSection.flightSpeed : null + showEnableCheckbox: true + enableCheckBoxChecked: defaultsRect._settingsItem ? defaultsRect._settingsItem.speedSection.specifyFlightSpeed : false + visible: defaultsRect._settingsItem ? defaultsRect._settingsItem.speedSection.available : false + + onEnableCheckboxClicked: { + if (defaultsRect._settingsItem) { + defaultsRect._settingsItem.speedSection.specifyFlightSpeed = enableCheckBoxChecked + } + } + } + + // ── Vehicle Info ── + SectionHeader { + id: vehicleInfoSectionHeader + Layout.fillWidth: true + text: qsTr("Vehicle Info") + visible: !defaultsRect._waypointsOnlyMode + checked: false + } + + GridLayout { + Layout.fillWidth: true + columnSpacing: ScreenTools.defaultFontPixelWidth + rowSpacing: columnSpacing + columns: 2 + visible: vehicleInfoSectionHeader.visible && vehicleInfoSectionHeader.checked + + QGCLabel { + text: qsTr("Firmware") + Layout.fillWidth: true + visible: defaultsRect._multipleFirmware + } + FactComboBox { + fact: QGroundControl.settingsManager.appSettings.offlineEditingFirmwareClass + indexModel: false + Layout.preferredWidth: defaultsRect._fieldWidth + visible: defaultsRect._multipleFirmware && defaultsRect._allowFWVehicleTypeSelection + } + QGCLabel { + text: defaultsRect._controllerVehicle ? defaultsRect._controllerVehicle.firmwareTypeString : "" + visible: defaultsRect._multipleFirmware && !defaultsRect._allowFWVehicleTypeSelection + } + + QGCLabel { + text: qsTr("Vehicle") + Layout.fillWidth: true + visible: defaultsRect._multipleVehicleTypes + } + FactComboBox { + fact: QGroundControl.settingsManager.appSettings.offlineEditingVehicleClass + indexModel: false + Layout.preferredWidth: defaultsRect._fieldWidth + visible: defaultsRect._multipleVehicleTypes && defaultsRect._allowFWVehicleTypeSelection + } + QGCLabel { + text: defaultsRect._controllerVehicle ? defaultsRect._controllerVehicle.vehicleTypeString : "" + visible: defaultsRect._multipleVehicleTypes && !defaultsRect._allowFWVehicleTypeSelection + } + + QGCLabel { + Layout.columnSpan: 2 + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("The following speed values are used to calculate total mission time. They do not affect the flight speed for the mission.") + visible: defaultsRect._showCruiseSpeed || defaultsRect._showHoverSpeed + } + + QGCLabel { + text: qsTr("Cruise speed") + visible: defaultsRect._showCruiseSpeed + Layout.fillWidth: true + } + FactTextField { + fact: QGroundControl.settingsManager.appSettings.offlineEditingCruiseSpeed + visible: defaultsRect._showCruiseSpeed + Layout.preferredWidth: defaultsRect._fieldWidth + } + + QGCLabel { + text: qsTr("Hover speed") + visible: defaultsRect._showHoverSpeed + Layout.fillWidth: true + } + FactTextField { + fact: QGroundControl.settingsManager.appSettings.offlineEditingHoverSpeed + visible: defaultsRect._showHoverSpeed + Layout.preferredWidth: defaultsRect._fieldWidth + } + } + + // ── Launch Position ── + SectionHeader { + id: plannedHomePositionSection + Layout.fillWidth: true + text: qsTr("Launch Position") + visible: !defaultsRect._vehicleHasHomePosition + checked: false + } + + GridLayout { + Layout.fillWidth: true + columnSpacing: ScreenTools.defaultFontPixelWidth + rowSpacing: columnSpacing + columns: 2 + visible: plannedHomePositionSection.checked && !defaultsRect._vehicleHasHomePosition + + QGCLabel { + text: qsTr("Altitude") + } + FactTextField { + fact: defaultsRect._settingsItem ? defaultsRect._settingsItem.plannedHomePositionAltitude : null + Layout.fillWidth: true + } + } + + QGCLabel { + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Actual position set by vehicle at flight time.") + horizontalAlignment: Text.AlignHCenter + visible: plannedHomePositionSection.checked && !defaultsRect._vehicleHasHomePosition + } + + QGCButton { + text: qsTr("Set To Map Center") + Layout.alignment: Qt.AlignHCenter + visible: plannedHomePositionSection.checked && !defaultsRect._vehicleHasHomePosition + onClicked: { + if (defaultsRect._settingsItem) { + defaultsRect._settingsItem.coordinate = root.editorMap.center + } + } + } + } + } + } + // ── Mission item delegate ── Component { id: missionItemComponent diff --git a/src/QmlControls/MissionSettingsEditor.qml b/src/QmlControls/MissionSettingsEditor.qml index adb2ccf93a8e..962be789cdf6 100644 --- a/src/QmlControls/MissionSettingsEditor.qml +++ b/src/QmlControls/MissionSettingsEditor.qml @@ -6,7 +6,7 @@ import QGroundControl import QGroundControl.Controls import QGroundControl.FactControls -// Editor for Mission Settings +// Editor for Initial Camera Settings (mission item 0) Rectangle { id: root width: availableWidth @@ -15,45 +15,14 @@ Rectangle { radius: ScreenTools.defaultBorderRadius property var _masterController: masterController - property var _missionController: _masterController.missionController property var _controllerVehicle: _masterController.controllerVehicle - property var _visualItems: _missionController.visualItems - property bool _vehicleHasHomePosition: _controllerVehicle.homePosition.isValid - property bool _showCruiseSpeed: !_controllerVehicle.multiRotor - property bool _showHoverSpeed: _controllerVehicle.multiRotor || _controllerVehicle.vtol - property bool _multipleFirmware: !QGroundControl.singleFirmwareSupport - property bool _multipleVehicleTypes: !QGroundControl.singleVehicleSupport - property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 16 - property bool _mobile: ScreenTools.isMobile - property var _savePath: QGroundControl.settingsManager.appSettings.missionSavePath - property var _appSettings: QGroundControl.settingsManager.appSettings property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly property bool _showCameraSection: _waypointsOnlyMode || QGroundControl.corePlugin.showAdvancedUI property bool _simpleMissionStart: QGroundControl.corePlugin.options.showSimpleMissionStart - property bool _showFlightSpeed: !_controllerVehicle.vtol && !_simpleMissionStart && !_controllerVehicle.apmFirmware - property bool _allowFWVehicleTypeSelection: _noMissionItemsAdded && !globals.activeVehicle - readonly property string _firmwareLabel: qsTr("Firmware") - readonly property string _vehicleLabel: qsTr("Vehicle") readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2 QGCPalette { id: qgcPal } - Component { id: altModeDialogComponent; AltModeDialog { } } - - QGCPopupDialogFactory { - id: altModeDialogFactory - - dialogComponent: altModeDialogComponent - } - - Connections { - target: _controllerVehicle - function onSupportsTerrainFrameChanged() { - if (!_controllerVehicle.supportsTerrainFrame && _missionController.globalAltitudeMode === QGroundControl.AltitudeModeTerrainFrame) { - _missionController.globalAltitudeMode = QGroundControl.AltitudeModeCalcAboveTerrain - } - } - } ColumnLayout { id: valuesColumn @@ -63,155 +32,6 @@ Rectangle { anchors.top: parent.top spacing: ScreenTools.defaultFontPixelHeight / 2 - LabelledButton { - Layout.fillWidth: true - label: qsTr("Altitude Mode") - buttonText: QGroundControl.altitudeModeShortDescription(_missionController.globalAltitudeMode) - - onClicked: { - var removeModes = [] - var updateFunction = function(altMode){ _missionController.globalAltitudeMode = altMode } - if (!_controllerVehicle.supportsTerrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) - } - if (!_noMissionItemsAdded) { - if (_missionController.globalAltitudeMode !== QGroundControl.AltitudeModeRelative) { - removeModes.push(QGroundControl.AltitudeModeRelative) - } - if (_missionController.globalAltitudeMode !== QGroundControl.AltitudeModeAbsolute) { - removeModes.push(QGroundControl.AltitudeModeAbsolute) - } - if (_missionController.globalAltitudeMode !== QGroundControl.AltitudeModeCalcAboveTerrain) { - removeModes.push(QGroundControl.AltitudeModeCalcAboveTerrain) - } - if (_missionController.globalAltitudeMode !== QGroundControl.AltitudeModeTerrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) - } - } - altModeDialogFactory.open({ rgRemoveModes: removeModes, updateAltModeFn: updateFunction }) - } - } - - FactTextFieldSlider { - Layout.fillWidth: true - label: qsTr("Waypoints Altitude") - fact: QGroundControl.settingsManager.appSettings.defaultMissionItemAltitude - } - - QGCButton { - id: applyDefaultAltitudeButton - Layout.fillWidth: true - text: qsTr("Apply New Altitude") - visible: false - - onClicked: QGroundControl.showMessageDialog(root, qsTr("Apply New Altitude"), - qsTr("You have changed the default altitude for mission items. Would you like to apply that altitude to all the items in the current mission?"), - Dialog.Yes | Dialog.No, - function() { applyDefaultAltitudeButton.visible = false; _missionController.applyDefaultMissionAltitude() }) - - Connections { - target: _appSettings.defaultMissionItemAltitude - function onRawValueChanged() { - if (_visualItems.count > 1) { - applyDefaultAltitudeButton.visible = true - } - } - } - - Connections { - target: _visualItems - function onCountChanged() { - if (_visualItems.count <= 1) { - applyDefaultAltitudeButton.visible = false - } - } - } - } - - FactTextFieldSlider { - Layout.fillWidth: true - label: qsTr("Flight Speed") - fact: missionItem.speedSection.flightSpeed - showEnableCheckbox: true - enableCheckBoxChecked: missionItem.speedSection.specifyFlightSpeed - visible: missionItem.speedSection.available - - onEnableCheckboxClicked: missionItem.speedSection.specifyFlightSpeed = enableCheckBoxChecked - } - - /* - Removed for now. May come back... - SectionHeader { - id: createFromTemplateSection - Layout.fillWidth: true - text: qsTr("Create Plan From Template") - checked: true - visible: !_masterController.containsItems && !_masterController.manualCreation - } - - GridLayout { - Layout.fillWidth: true - columns: 2 - columnSpacing: ScreenTools.defaultFontPixelWidth * 0.75 - rowSpacing: columnSpacing - visible: createFromTemplateSection.visible && createFromTemplateSection.checked - - Repeater { - model: _masterController.planCreators - - Rectangle { - id: planCreatorButton - Layout.fillWidth: true - implicitHeight: planCreatorLayout.implicitHeight - color: planCreatorButtonMouseArea.pressed || planCreatorButtonMouseArea.containsMouse ? qgcPal.buttonHighlight : qgcPal.button - - ColumnLayout { - id: planCreatorLayout - width: parent.width - spacing: 0 - - Image { - id: planCreatorImage - Layout.fillWidth: true - source: object.imageResource - sourceSize.width: width - fillMode: Image.PreserveAspectFit - mipmap: true - } - - QGCLabel { - id: planCreatorNameLabel - Layout.fillWidth: true - Layout.maximumWidth: parent.width - horizontalAlignment: Text.AlignHCenter - text: object.name - color: planCreatorButtonMouseArea.pressed || planCreatorButtonMouseArea.containsMouse ? qgcPal.buttonHighlightText : qgcPal.buttonText - } - } - - QGCMouseArea { - id: planCreatorButtonMouseArea - anchors.fill: parent - hoverEnabled: true - preventStealing: true - onClicked: { - if (object.blankPlan) { - _masterController.manualCreation = true - } else { - object.createPlan(_mapCenter()) - } - } - - function _mapCenter() { - var centerPoint = Qt.point(editorMap.centerViewport.left + (editorMap.centerViewport.width / 2), editorMap.centerViewport.top + (editorMap.centerViewport.height / 2)) - return editorMap.toCoordinate(centerPoint, false) - } - } - } - } - } - */ - Column { Layout.fillWidth: true spacing: _margin @@ -222,6 +42,7 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right visible: _showCameraSection + showSectionHeader: false Component.onCompleted: checked = !_waypointsOnlyMode && missionItem.cameraSection.settingsSpecified } @@ -235,134 +56,6 @@ Rectangle { font.pointSize: ScreenTools.smallFontPointSize visible: _showCameraSection && cameraSection.checked } - - SectionHeader { - id: vehicleInfoSectionHeader - anchors.left: parent.left - anchors.right: parent.right - text: qsTr("Vehicle Info") - visible: !_waypointsOnlyMode - checked: false - } - - GridLayout { - anchors.left: parent.left - anchors.right: parent.right - columnSpacing: ScreenTools.defaultFontPixelWidth - rowSpacing: columnSpacing - columns: 2 - visible: vehicleInfoSectionHeader.visible && vehicleInfoSectionHeader.checked - - QGCLabel { - text: _firmwareLabel - Layout.fillWidth: true - visible: _multipleFirmware - } - FactComboBox { - fact: QGroundControl.settingsManager.appSettings.offlineEditingFirmwareClass - indexModel: false - Layout.preferredWidth: _fieldWidth - visible: _multipleFirmware && _allowFWVehicleTypeSelection - } - QGCLabel { - text: _controllerVehicle.firmwareTypeString - visible: _multipleFirmware && !_allowFWVehicleTypeSelection - } - - QGCLabel { - text: _vehicleLabel - Layout.fillWidth: true - visible: _multipleVehicleTypes - } - FactComboBox { - fact: QGroundControl.settingsManager.appSettings.offlineEditingVehicleClass - indexModel: false - Layout.preferredWidth: _fieldWidth - visible: _multipleVehicleTypes && _allowFWVehicleTypeSelection - } - QGCLabel { - text: _controllerVehicle.vehicleTypeString - visible: _multipleVehicleTypes && !_allowFWVehicleTypeSelection - } - - QGCLabel { - Layout.columnSpan: 2 - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - wrapMode: Text.WordWrap - font.pointSize: ScreenTools.smallFontPointSize - text: qsTr("The following speed values are used to calculate total mission time. They do not affect the flight speed for the mission.") - visible: _showCruiseSpeed || _showHoverSpeed - } - - QGCLabel { - text: qsTr("Cruise speed") - visible: _showCruiseSpeed - Layout.fillWidth: true - } - FactTextField { - fact: QGroundControl.settingsManager.appSettings.offlineEditingCruiseSpeed - visible: _showCruiseSpeed - Layout.preferredWidth: _fieldWidth - } - - QGCLabel { - text: qsTr("Hover speed") - visible: _showHoverSpeed - Layout.fillWidth: true - } - FactTextField { - fact: QGroundControl.settingsManager.appSettings.offlineEditingHoverSpeed - visible: _showHoverSpeed - Layout.preferredWidth: _fieldWidth - } - } // GridLayout - - SectionHeader { - id: plannedHomePositionSection - anchors.left: parent.left - anchors.right: parent.right - text: qsTr("Launch Position") - visible: !_vehicleHasHomePosition - checked: false - } - - Column { - anchors.left: parent.left - anchors.right: parent.right - spacing: _margin - visible: plannedHomePositionSection.checked && !_vehicleHasHomePosition - - GridLayout { - anchors.left: parent.left - anchors.right: parent.right - columnSpacing: ScreenTools.defaultFontPixelWidth - rowSpacing: columnSpacing - columns: 2 - - QGCLabel { - text: qsTr("Altitude") - } - FactTextField { - fact: missionItem.plannedHomePositionAltitude - Layout.fillWidth: true - } - } - - QGCLabel { - width: parent.width - wrapMode: Text.WordWrap - font.pointSize: ScreenTools.smallFontPointSize - text: qsTr("Actual position set by vehicle at flight time.") - horizontalAlignment: Text.AlignHCenter - } - - QGCButton { - text: qsTr("Set To Map Center") - onClicked: missionItem.coordinate = map.center - anchors.horizontalCenter: parent.horizontalCenter - } - } - } // Column - } // Column -} // Rectangle + } + } +} From 308ebdf09c42b919e669475ce43d71ff448dced8 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 6 Mar 2026 10:00:26 -0800 Subject: [PATCH 034/119] Plan: add file name management to PlanMasterController Add writable currentPlanFileName, originalPlanFileName, and planFileRenamed properties to PlanMasterController so the UI can display and edit the plan file name directly in the tree view. New properties and methods: - currentPlanFileName (Q_PROPERTY, read/write) - user-editable file name - originalPlanFileName (Q_PROPERTY, read-only) - name from last load/save - planFileRenamed (Q_PROPERTY, read-only) - true when names differ - saveWithCurrentName() - saves using the current name in the same directory - resolvedPlanFileExists() - checks if the resolved path already exists Implementation details: - setCurrentPlanFileName sanitizes input (strips /\:*?"<>| characters) - Signal emissions are guarded to avoid spurious notifications - Extract _resolvedPlanFilePath() and _clearFileNames() private helpers to eliminate duplicated path-resolution and clear logic - loadFromFile, saveToFile, removeAll, and removeAllFromVehicle all maintain file name state consistently PlanToolBarIndicators.qml: - Update save flow to use saveWithCurrentName for renamed files - Show resolved file existence check before overwrite --- src/MissionManager/PlanMasterController.cc | 128 ++++++++++++++++++++- src/MissionManager/PlanMasterController.h | 22 +++- src/QmlControls/PlanToolBarIndicators.qml | 28 +++-- 3 files changed, 164 insertions(+), 14 deletions(-) diff --git a/src/MissionManager/PlanMasterController.cc b/src/MissionManager/PlanMasterController.cc index 3947578b059a..427adf5280b2 100644 --- a/src/MissionManager/PlanMasterController.cc +++ b/src/MissionManager/PlanMasterController.cc @@ -22,8 +22,9 @@ #include #include -#include #include +#include +#include QGC_LOGGING_CATEGORY(PlanMasterControllerLog, "PlanManager.PlanMasterController") @@ -390,13 +391,45 @@ void PlanMasterController::loadFromFile(const QString& filename) } } - if(success){ + if (success){ + const bool oldRenamed = planFileRenamed(); _currentPlanFile = QString::asprintf("%s/%s.%s", fileInfo.path().toLocal8Bit().data(), fileInfo.completeBaseName().toLocal8Bit().data(), AppSettings::planFileExtension); + const bool currentNameChanged = (_currentPlanFileName != fileInfo.completeBaseName()); + const bool originalNameChanged = (_originalPlanFileName != fileInfo.completeBaseName()); + _currentPlanFileName = fileInfo.completeBaseName(); + _originalPlanFileName = _currentPlanFileName; _setDirtyStates(false /* dirtyForSave */, true /* dirtyForUpload */); + emit currentPlanFileChanged(); + if (currentNameChanged) { + emit currentPlanFileNameChanged(); + } + if (originalNameChanged) { + emit originalPlanFileNameChanged(); + } + if (oldRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } } else { + const bool hadFile = !_currentPlanFile.isEmpty(); + const bool hadCurrentName = !_currentPlanFileName.isEmpty(); + const bool hadOriginalName = !_originalPlanFileName.isEmpty(); + const bool wasRenamed = planFileRenamed(); _currentPlanFile.clear(); + _currentPlanFileName.clear(); + _originalPlanFileName.clear(); + if (hadFile) { + emit currentPlanFileChanged(); + } + if (hadCurrentName) { + emit currentPlanFileNameChanged(); + } + if (hadOriginalName) { + emit originalPlanFileNameChanged(); + } + if (wasRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } } - emit currentPlanFileChanged(); } QJsonDocument PlanMasterController::saveToJson() @@ -459,6 +492,19 @@ bool PlanMasterController::saveToFile(const QString& filename) _currentPlanFile = planFilename; emit currentPlanFileChanged(); } + const bool wasRenamed = planFileRenamed(); + const QString savedBaseName = QFileInfo(planFilename).completeBaseName(); + if (_currentPlanFileName != savedBaseName) { + _currentPlanFileName = savedBaseName; + emit currentPlanFileNameChanged(); + } + if (_originalPlanFileName != savedBaseName) { + _originalPlanFileName = savedBaseName; + emit originalPlanFileNameChanged(); + } + if (wasRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } _setDirtyForSave(false); } @@ -502,8 +548,7 @@ void PlanMasterController::removeAll(void) _setDirtyStates(false, false); if (_offline) { - _currentPlanFile.clear(); - emit currentPlanFileChanged(); + _clearFileNames(); } setManualCreation(false); } @@ -519,6 +564,7 @@ void PlanMasterController::removeAllFromVehicle(void) _rallyPointController.removeAllFromVehicle(); } _setDirtyForUpload(false); + _clearFileNames(); } else { qCCritical(PlanMasterControllerLog) << "PlanMasterController::removeAllFromVehicle called while offline"; } @@ -535,6 +581,78 @@ QString PlanMasterController::fileExtension(void) const return AppSettings::planFileExtension; } +void PlanMasterController::setCurrentPlanFileName(const QString& name) +{ + // Normalize to a base name: trim whitespace, strip known extension, remove illegal characters + QString sanitized = name.trimmed(); + const QString ext = QStringLiteral(".") + fileExtension(); + if (sanitized.endsWith(ext, Qt::CaseInsensitive)) { + sanitized.chop(ext.length()); + sanitized = sanitized.trimmed(); + } + sanitized.remove(QRegularExpression(QStringLiteral("[/\\\\:*?\"<>|]"))); + if (_currentPlanFileName != sanitized) { + const bool wasRenamed = planFileRenamed(); + _currentPlanFileName = sanitized; + emit currentPlanFileNameChanged(); + if (wasRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } + } +} + +bool PlanMasterController::saveWithCurrentName() +{ + if (_currentPlanFileName.isEmpty()) { + return false; + } + return saveToFile(_resolvedPlanFilePath()); +} + +bool PlanMasterController::planFileRenamed() const +{ + return !_originalPlanFileName.isEmpty() && _currentPlanFileName != _originalPlanFileName; +} + +bool PlanMasterController::resolvedPlanFileExists() const +{ + if (_currentPlanFileName.isEmpty()) { + return false; + } + return QFile::exists(_resolvedPlanFilePath()); +} + +QString PlanMasterController::_resolvedPlanFilePath() const +{ + const QString dir = _currentPlanFile.isEmpty() + ? SettingsManager::instance()->appSettings()->missionSavePath() + : QFileInfo(_currentPlanFile).path(); + return QStringLiteral("%1/%2.%3").arg(dir, _currentPlanFileName, fileExtension()); +} + +void PlanMasterController::_clearFileNames() +{ + const bool hadFile = !_currentPlanFile.isEmpty(); + const bool hadCurrentName = !_currentPlanFileName.isEmpty(); + const bool hadOriginalName = !_originalPlanFileName.isEmpty(); + const bool wasRenamed = planFileRenamed(); + _currentPlanFile.clear(); + _currentPlanFileName.clear(); + _originalPlanFileName.clear(); + if (hadFile) { + emit currentPlanFileChanged(); + } + if (hadCurrentName) { + emit currentPlanFileNameChanged(); + } + if (hadOriginalName) { + emit originalPlanFileNameChanged(); + } + if (wasRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } +} + QString PlanMasterController::kmlFileExtension(void) const { return AppSettings::kmlFileExtension; diff --git a/src/MissionManager/PlanMasterController.h b/src/MissionManager/PlanMasterController.h index db9560976cca..9cbe6cb86226 100644 --- a/src/MissionManager/PlanMasterController.h +++ b/src/MissionManager/PlanMasterController.h @@ -54,7 +54,9 @@ class PlanMasterController : public QObject Q_PROPERTY(QString fileExtension READ fileExtension CONSTANT) ///< File extension for missions Q_PROPERTY(QString kmlFileExtension READ kmlFileExtension CONSTANT) Q_PROPERTY(QString currentPlanFile READ currentPlanFile NOTIFY currentPlanFileChanged) ///< Fully qualified path, empty if not yet saved - Q_PROPERTY(QString currentPlanFileName READ currentPlanFileName NOTIFY currentPlanFileChanged) ///< File name only, no path or extension + Q_PROPERTY(QString currentPlanFileName READ currentPlanFileName WRITE setCurrentPlanFileName NOTIFY currentPlanFileNameChanged) ///< Editable base name, no path or extension + Q_PROPERTY(QString originalPlanFileName READ originalPlanFileName NOTIFY originalPlanFileNameChanged) ///< On-disk name when last loaded/saved + Q_PROPERTY(bool planFileRenamed READ planFileRenamed NOTIFY planFileRenamedChanged) ///< true if currentPlanFileName differs from originalPlanFileName Q_PROPERTY(QStringList loadNameFilters READ loadNameFilters CONSTANT) ///< File filter list loading plan files Q_PROPERTY(QStringList saveNameFilters READ saveNameFilters CONSTANT) ///< File filter list saving plan files Q_PROPERTY(QmlObjectListModel* planCreators MEMBER _planCreators NOTIFY planCreatorsChanged) @@ -92,6 +94,9 @@ class PlanMasterController : public QObject Q_INVOKABLE bool saveToCurrent(); Q_INVOKABLE bool saveToFile(const QString& filename); Q_INVOKABLE void saveToKml(const QString& filename); + + Q_INVOKABLE bool saveWithCurrentName(); ///< Save using the (possibly renamed) currentPlanFileName + Q_INVOKABLE bool resolvedPlanFileExists() const; ///< true if a file at the renamed path already exists on disk Q_INVOKABLE void removeAll(void); ///< Removes all from controller only, synce required to remove from vehicle Q_INVOKABLE void removeAllFromVehicle(void); ///< Removes all from vehicle and controller @@ -107,7 +112,10 @@ class PlanMasterController : public QObject QString fileExtension (void) const; QString kmlFileExtension(void) const; QString currentPlanFile (void) const { return _currentPlanFile; } - QString currentPlanFileName (void) const { return QFileInfo(_currentPlanFile).completeBaseName(); } + QString currentPlanFileName (void) const { return _currentPlanFileName; } + void setCurrentPlanFileName(const QString& name); + QString originalPlanFileName(void) const { return _originalPlanFileName; } + bool planFileRenamed(void) const; QStringList loadNameFilters (void) const; QStringList saveNameFilters (void) const; bool isEmpty (void) const; @@ -134,6 +142,9 @@ class PlanMasterController : public QObject void dirtyForUploadChanged (bool dirtyForUpload); void offlineChanged (bool offlineEditing); void currentPlanFileChanged (void); + void currentPlanFileNameChanged (void); + void originalPlanFileNameChanged (void); + void planFileRenamedChanged (void); void planCreatorsChanged (QmlObjectListModel* planCreators); void managerVehicleChanged (Vehicle* managerVehicle); void promptForPlanUsageOnVehicleChange (void); @@ -157,6 +168,8 @@ private slots: void _setDirtyForSave(bool dirtyForSave); void _setDirtyForUpload(bool dirtyForUpload); void _setDirtyStates(bool dirtyForSave, bool dirtyForUpload); + QString _resolvedPlanFilePath() const; + void _clearFileNames(); #ifdef QGC_UNITTEST_BUILD // Used by unit tests to set dirty flags for initial state @@ -177,6 +190,11 @@ private slots: bool _sendGeoFence = false; bool _sendRallyPoints = false; QString _currentPlanFile; + // NOTE: _currentPlanFileName and _originalPlanFileName must be kept in sync + // with _currentPlanFile across all code paths that modify any of them: + // loadFromFile, saveToFile, removeAll, removeAllFromVehicle, _clearFileNames. + QString _currentPlanFileName; + QString _originalPlanFileName; bool _deleteWhenSendCompleted = false; bool _dirtyForSave = false; bool _dirtyForUpload = false; diff --git a/src/QmlControls/PlanToolBarIndicators.qml b/src/QmlControls/PlanToolBarIndicators.qml index 79567cbe87f3..107f4e14189c 100644 --- a/src/QmlControls/PlanToolBarIndicators.qml +++ b/src/QmlControls/PlanToolBarIndicators.qml @@ -54,14 +54,28 @@ RowLayout { } function _saveButtonClicked() { - if(_planMasterController.currentPlanFile !== "") { - if (_planMasterController.saveToCurrent()) { - QGroundControl.showMessageDialog(root, qsTr("Save"), - qsTr("Plan saved to `%1`").arg(_planMasterController.currentPlanFile), - Dialog.Ok) + if (_planMasterController.currentPlanFileName === "") { + if (_planMasterController.currentPlanFile === "") { + // No file and no name typed — open the file dialog + _planMasterController.saveToSelectedFile() + } else { + // Have a file but name was cleared — save to the existing file + _planMasterController.saveToCurrent() } + return + } + + if (_planMasterController.currentPlanFile === "" || _planMasterController.planFileRenamed) { + // First save with a typed name, or name was changed since last save + let fullName = _planMasterController.currentPlanFileName + "." + _planMasterController.fileExtension + let msg = _planMasterController.resolvedPlanFileExists() + ? qsTr("'%1' already exists. Overwrite?").arg(fullName) + : qsTr("Save as '%1'?").arg(fullName) + QGroundControl.showMessageDialog(root, qsTr("Save"), msg, + Dialog.Yes | Dialog.No, + function() { _planMasterController.saveWithCurrentName() }) } else { - _planMasterController.saveToSelectedFile() + _planMasterController.saveToCurrent() } } @@ -106,7 +120,7 @@ RowLayout { } QGCButton { - text: _planMasterController.currentPlanFile === "" ? qsTr("Save As") : _planMasterController.currentPlanFileName + text: qsTr("Save") iconSource: "/res/SaveToDisk.svg" enabled: !_syncInProgress && _hasPlanItems primary: _saveDirty From 15befe4582940b3a4fb0aa05f9da7c0296369289 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 6 Mar 2026 10:00:49 -0800 Subject: [PATCH 035/119] Tests: add file name and tree model tests for PlanView changes MissionControllerTreeTest: - Update for 5-group tree (Plan File, Defaults, Mission, GeoFence, Rally) - Reference MissionController::kPlanFileGroupRow etc. instead of local magic numbers to stay in sync with the source of truth PlanMasterControllerTest - new test methods: - _testFileNamesSetOnLoad: verify both names set after loadFromFile - _testCurrentPlanFileNameWritable: verify rename via property setter - _testPlanFileRenamed: verify renamed state tracking - _testSaveWithCurrentName: verify save-as with renamed file - _testSaveWithCurrentNameNoFile: verify save to default directory - _testResolvedPlanFileExists: verify file existence check - _testFileNamesClearedOnRemoveAll: verify names cleared on removeAll - _testFileNamesClearedOnRemoveAllFromVehicle: same for online clear - _testSaveUpdatesOriginalFileName: verify save updates original name Test infrastructure improvements: - Use QTemporaryDir instead of manual mkpath/rmdir for reliable cleanup --- .../MissionControllerTreeTest.cc | 62 ++++-- .../MissionControllerTreeTest.h | 11 +- .../PlanMasterControllerTest.cc | 195 ++++++++++++++++++ .../MissionManager/PlanMasterControllerTest.h | 11 + 4 files changed, 258 insertions(+), 21 deletions(-) diff --git a/test/MissionManager/MissionControllerTreeTest.cc b/test/MissionManager/MissionControllerTreeTest.cc index 475b09833971..957f1bc4fe63 100644 --- a/test/MissionManager/MissionControllerTreeTest.cc +++ b/test/MissionManager/MissionControllerTreeTest.cc @@ -52,21 +52,37 @@ void MissionControllerTreeTest::_testTreeStructureAfterInit() QmlObjectTreeModel* tree = _missionController->visualItemsTree(); QVERIFY(tree); - // Root should have 3 top-level groups: Mission Items, GeoFence, Rally Points - QCOMPARE(tree->rowCount(), 3); + // Root should have all expected top-level groups + QCOMPARE(tree->rowCount(), kGroupCount); // Verify group node types - const QModelIndex missionGroup = tree->index(0, 0); - const QModelIndex fenceGroup = tree->index(1, 0); - const QModelIndex rallyGroup = tree->index(2, 0); + const QModelIndex planFileGroup = tree->index(kPlanFileGroupRow, 0); + const QModelIndex defaultsGroup = tree->index(kDefaultsGroupRow, 0); + const QModelIndex missionGroup = tree->index(kMissionGroupRow, 0); + const QModelIndex fenceGroup = tree->index(kFenceGroupRow, 0); + const QModelIndex rallyGroup = tree->index(kRallyGroupRow, 0); + QVERIFY(planFileGroup.isValid()); + QVERIFY(defaultsGroup.isValid()); QVERIFY(missionGroup.isValid()); QVERIFY(fenceGroup.isValid()); QVERIFY(rallyGroup.isValid()); + QCOMPARE(tree->data(planFileGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("planFileGroup")); + QCOMPARE(tree->data(defaultsGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("defaultsGroup")); QCOMPARE(tree->data(missionGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("missionGroup")); QCOMPARE(tree->data(fenceGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("fenceGroup")); QCOMPARE(tree->data(rallyGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("rallyGroup")); + // Plan File group should have 1 child (planFileInfo marker) + QCOMPARE(tree->rowCount(planFileGroup), 1); + const QModelIndex planFileChild = tree->index(0, 0, planFileGroup); + QCOMPARE(tree->data(planFileChild, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("planFileInfo")); + + // Defaults group should have 1 child (defaultsInfo marker) + QCOMPARE(tree->rowCount(defaultsGroup), 1); + const QModelIndex defaultsChild = tree->index(0, 0, defaultsGroup); + QCOMPARE(tree->data(defaultsChild, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("defaultsInfo")); + // Mission group should contain 1 child (the MissionSettingsItem) QCOMPARE(tree->rowCount(missionGroup), 1); @@ -90,7 +106,7 @@ void MissionControllerTreeTest::_testInsertWaypointUpdatesTree() _initForTest(); QmlObjectTreeModel* tree = _missionController->visualItemsTree(); - const QModelIndex missionGroup = tree->index(0, 0); + const QModelIndex missionGroup = tree->index(kMissionGroupRow, 0); // Before insert: 1 mission item (MissionSettingsItem) const int initialMissionChildren = tree->rowCount(missionGroup); @@ -126,7 +142,7 @@ void MissionControllerTreeTest::_testRemoveWaypointUpdatesTree() _initForTest(); QmlObjectTreeModel* tree = _missionController->visualItemsTree(); - const QModelIndex missionGroup = tree->index(0, 0); + const QModelIndex missionGroup = tree->index(kMissionGroupRow, 0); // Insert 3 waypoints const QList waypoints = Coord::waypointPath(Coord::zurich(), 3); @@ -166,17 +182,17 @@ void MissionControllerTreeTest::_testRemoveAllRebuildsTree() _missionController->insertSimpleMissionItem(waypoints[i], i + 1); } - const QModelIndex missionGroupBefore = tree->index(0, 0); + const QModelIndex missionGroupBefore = tree->index(kMissionGroupRow, 0); QCOMPARE(tree->rowCount(missionGroupBefore), 4); // Remove all _missionController->removeAll(); - // After removeAll, tree should still have 3 groups - QCOMPARE(tree->rowCount(), 3); + // After removeAll, tree should still have all groups + QCOMPARE(tree->rowCount(), kGroupCount); // Mission group should have 1 child (the new MissionSettingsItem) - const QModelIndex missionGroupAfter = tree->index(0, 0); + const QModelIndex missionGroupAfter = tree->index(kMissionGroupRow, 0); QCOMPARE(tree->rowCount(missionGroupAfter), 1); // That child should be the new settings item @@ -196,9 +212,11 @@ void MissionControllerTreeTest::_testPersistentGroupIndexes() QmlObjectTreeModel* tree = _missionController->visualItemsTree(); // Capture persistent indexes for each group - QPersistentModelIndex missionPersist(tree->index(0, 0)); - QPersistentModelIndex fencePersist(tree->index(1, 0)); - QPersistentModelIndex rallyPersist(tree->index(2, 0)); + QPersistentModelIndex planFilePersist(tree->index(kPlanFileGroupRow, 0)); + QPersistentModelIndex defaultsPersist(tree->index(kDefaultsGroupRow, 0)); + QPersistentModelIndex missionPersist(tree->index(kMissionGroupRow, 0)); + QPersistentModelIndex fencePersist(tree->index(kFenceGroupRow, 0)); + QPersistentModelIndex rallyPersist(tree->index(kRallyGroupRow, 0)); // Insert waypoints const QList waypoints = Coord::waypointPath(Coord::zurich(), 5); @@ -207,20 +225,26 @@ void MissionControllerTreeTest::_testPersistentGroupIndexes() } // All persistent indexes should still be valid + QVERIFY(planFilePersist.isValid()); + QVERIFY(defaultsPersist.isValid()); QVERIFY(missionPersist.isValid()); QVERIFY(fencePersist.isValid()); QVERIFY(rallyPersist.isValid()); // They should still be at the same rows (group structure didn't change) - QCOMPARE(missionPersist.row(), 0); - QCOMPARE(fencePersist.row(), 1); - QCOMPARE(rallyPersist.row(), 2); + QCOMPARE(planFilePersist.row(), kPlanFileGroupRow); + QCOMPARE(defaultsPersist.row(), kDefaultsGroupRow); + QCOMPARE(missionPersist.row(), kMissionGroupRow); + QCOMPARE(fencePersist.row(), kFenceGroupRow); + QCOMPARE(rallyPersist.row(), kRallyGroupRow); // Remove some waypoints _missionController->removeVisualItem(3); _missionController->removeVisualItem(2); // Persistent indexes should still be valid + QVERIFY(planFilePersist.isValid()); + QVERIFY(defaultsPersist.isValid()); QVERIFY(missionPersist.isValid()); QVERIFY(fencePersist.isValid()); QVERIFY(rallyPersist.isValid()); @@ -241,14 +265,14 @@ void MissionControllerTreeTest::_testRecalcChildItemsNoCrash() } QmlObjectTreeModel* tree = _missionController->visualItemsTree(); - const QModelIndex missionGroup = tree->index(0, 0); + const QModelIndex missionGroup = tree->index(kMissionGroupRow, 0); // Tree should be consistent — recalcChildItems happens automatically during insert QCOMPARE(tree->rowCount(missionGroup), _missionController->visualItems()->count()); // Verify no crash and tree is still functional QVERIFY(tree->index(0, 0, missionGroup).isValid()); - QCOMPARE(tree->rowCount(), 3); // 3 groups + QCOMPARE(tree->rowCount(), kGroupCount); } #include "UnitTest.h" diff --git a/test/MissionManager/MissionControllerTreeTest.h b/test/MissionManager/MissionControllerTreeTest.h index a3134320adcd..81951cc78c08 100644 --- a/test/MissionManager/MissionControllerTreeTest.h +++ b/test/MissionManager/MissionControllerTreeTest.h @@ -3,8 +3,7 @@ #include #include "MissionControllerManagerTest.h" - -class MissionController; +#include "MissionController.h" class PlanMasterController; /// Tests for the MissionController incremental tree model sync (PlanTreeView branch). @@ -41,6 +40,14 @@ private slots: private: void _initForTest(); + // Use canonical constants from MissionController + static constexpr int kPlanFileGroupRow = MissionController::kPlanFileGroupRow; + static constexpr int kDefaultsGroupRow = MissionController::kDefaultsGroupRow; + static constexpr int kMissionGroupRow = MissionController::kMissionGroupRow; + static constexpr int kFenceGroupRow = MissionController::kFenceGroupRow; + static constexpr int kRallyGroupRow = MissionController::kRallyGroupRow; + static constexpr int kGroupCount = MissionController::kGroupCount; + std::unique_ptr _masterController; MissionController* _missionController = nullptr; }; diff --git a/test/MissionManager/PlanMasterControllerTest.cc b/test/MissionManager/PlanMasterControllerTest.cc index 0e97fd993d80..c1f74f16fc06 100644 --- a/test/MissionManager/PlanMasterControllerTest.cc +++ b/test/MissionManager/PlanMasterControllerTest.cc @@ -1,14 +1,17 @@ #include "PlanMasterControllerTest.h" +#include "AppSettings.h" #include "MissionManager.h" #include "MultiSignalSpy.h" #include "MultiVehicleManager.h" #include "PlanMasterController.h" +#include "SettingsManager.h" #include "Vehicle.h" #include #include #include +#include #include void PlanMasterControllerTest::init() @@ -248,6 +251,198 @@ void PlanMasterControllerTest::_testDirtyFlagsMatrix() } } +// =========================================================================== +// File name property tests +// =========================================================================== + +void PlanMasterControllerTest::_testFileNamesSetOnLoad() +{ + QSignalSpy currentNameSpy(_masterController, &PlanMasterController::currentPlanFileNameChanged); + QSignalSpy originalNameSpy(_masterController, &PlanMasterController::originalPlanFileNameChanged); + + // Before load, names should be empty + QVERIFY(_masterController->currentPlanFileName().isEmpty()); + QVERIFY(_masterController->originalPlanFileName().isEmpty()); + + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // After successful load, both names should be set to the base name + QCOMPARE(_masterController->currentPlanFileName(), QStringLiteral("MissionPlanner")); + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("MissionPlanner")); + + // Signals should have fired + QVERIFY(currentNameSpy.count() >= 1); + QVERIFY(originalNameSpy.count() >= 1); +} + +void PlanMasterControllerTest::_testCurrentPlanFileNameWritable() +{ + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + QSignalSpy currentNameSpy(_masterController, &PlanMasterController::currentPlanFileNameChanged); + QSignalSpy originalNameSpy(_masterController, &PlanMasterController::originalPlanFileNameChanged); + + // Rename via the writable property + _masterController->setCurrentPlanFileName(QStringLiteral("RenamedPlan")); + + QCOMPARE(_masterController->currentPlanFileName(), QStringLiteral("RenamedPlan")); + // Original should remain unchanged + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("MissionPlanner")); + + QCOMPARE(currentNameSpy.count(), 1); + QCOMPARE(originalNameSpy.count(), 0); + + // Setting to the same value should not emit again + _masterController->setCurrentPlanFileName(QStringLiteral("RenamedPlan")); + QCOMPARE(currentNameSpy.count(), 1); +} + +void PlanMasterControllerTest::_testPlanFileRenamed() +{ + // Before load, planFileRenamed should be false (both names empty) + QVERIFY(!_masterController->planFileRenamed()); + + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // After load, current == original → not renamed + QVERIFY(!_masterController->planFileRenamed()); + + // Rename + _masterController->setCurrentPlanFileName(QStringLiteral("NewName")); + QVERIFY(_masterController->planFileRenamed()); + + // Rename back to original + _masterController->setCurrentPlanFileName(QStringLiteral("MissionPlanner")); + QVERIFY(!_masterController->planFileRenamed()); +} + +void PlanMasterControllerTest::_testSaveWithCurrentName() +{ + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // First save to a real (writable) directory so _currentPlanFile points somewhere valid + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + const QString initialPath = QStringLiteral("%1/MissionPlanner.%2").arg(tmpDir.path(), _masterController->fileExtension()); + QVERIFY(_masterController->saveToFile(initialPath)); + + // Rename + _masterController->setCurrentPlanFileName(QStringLiteral("TestSaveRenamed")); + + // Save with the renamed name + QVERIFY(_masterController->saveWithCurrentName()); + + // After save, original should now match the renamed name + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("TestSaveRenamed")); + QCOMPARE(_masterController->currentPlanFileName(), QStringLiteral("TestSaveRenamed")); + QVERIFY(!_masterController->planFileRenamed()); +} + +void PlanMasterControllerTest::_testSaveWithCurrentNameNoFile() +{ + // No file loaded — saveWithCurrentName with empty name should fail + QVERIFY(!_masterController->saveWithCurrentName()); + + // Set a name without loading a file first (simulates typing a name in the UI) + _masterController->setCurrentPlanFileName(QStringLiteral("BrandNewPlan")); + + // Should save to the default mission save directory + QVERIFY(_masterController->saveWithCurrentName()); + + const QString expectedDir = SettingsManager::instance()->appSettings()->missionSavePath(); + const QString expectedPath = QStringLiteral("%1/BrandNewPlan.%2").arg(expectedDir, _masterController->fileExtension()); + QCOMPARE(_masterController->currentPlanFile(), expectedPath); + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("BrandNewPlan")); + + // Clean up + QFile::remove(expectedPath); +} + +void PlanMasterControllerTest::_testResolvedPlanFileExists() +{ + // Empty name → should return false + QVERIFY(!_masterController->resolvedPlanFileExists()); + + // Save a file so it exists on disk + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + const QString savePath = QStringLiteral("%1/ExistingPlan.%2").arg(tmpDir.path(), _masterController->fileExtension()); + QVERIFY(_masterController->saveToFile(savePath)); + + // Now rename to the same base name — file exists at resolved path + _masterController->setCurrentPlanFileName(QStringLiteral("ExistingPlan")); + QVERIFY(_masterController->resolvedPlanFileExists()); + + // Rename to something non-existent + _masterController->setCurrentPlanFileName(QStringLiteral("DoesNotExist")); + QVERIFY(!_masterController->resolvedPlanFileExists()); +} + +void PlanMasterControllerTest::_testFileNamesClearedOnRemoveAll() +{ + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // Verify names are set + QVERIFY(!_masterController->currentPlanFileName().isEmpty()); + QVERIFY(!_masterController->originalPlanFileName().isEmpty()); + + QSignalSpy currentNameSpy(_masterController, &PlanMasterController::currentPlanFileNameChanged); + QSignalSpy originalNameSpy(_masterController, &PlanMasterController::originalPlanFileNameChanged); + + _masterController->removeAll(); + + // Names should be cleared + QVERIFY(_masterController->currentPlanFileName().isEmpty()); + QVERIFY(_masterController->originalPlanFileName().isEmpty()); + QVERIFY(_masterController->currentPlanFile().isEmpty()); + + QVERIFY(currentNameSpy.count() >= 1); + QVERIFY(originalNameSpy.count() >= 1); +} + +void PlanMasterControllerTest::_testFileNamesClearedOnRemoveAllFromVehicle() +{ + _connectMockLink(MAV_AUTOPILOT_PX4); + + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // Verify names are set + QVERIFY(!_masterController->currentPlanFileName().isEmpty()); + QVERIFY(!_masterController->originalPlanFileName().isEmpty()); + + QSignalSpy currentNameSpy(_masterController, &PlanMasterController::currentPlanFileNameChanged); + QSignalSpy originalNameSpy(_masterController, &PlanMasterController::originalPlanFileNameChanged); + + _masterController->removeAllFromVehicle(); + + // Names should be cleared + QVERIFY(_masterController->currentPlanFileName().isEmpty()); + QVERIFY(_masterController->originalPlanFileName().isEmpty()); + QVERIFY(_masterController->currentPlanFile().isEmpty()); + + QVERIFY(currentNameSpy.count() >= 1); + QVERIFY(originalNameSpy.count() >= 1); +} + +void PlanMasterControllerTest::_testSaveUpdatesOriginalFileName() +{ + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("MissionPlanner")); + + // Save to a completely different path + const QString saveFile = QDir::temp().filePath( + QStringLiteral("qgc_planmaster_rename_%1.plan").arg(QDateTime::currentMSecsSinceEpoch())); + QVERIFY(_masterController->saveToFile(saveFile)); + + // Both names should now reflect the new file base name + const QString expectedBase = QFileInfo(saveFile).completeBaseName(); + QCOMPARE(_masterController->currentPlanFileName(), expectedBase); + QCOMPARE(_masterController->originalPlanFileName(), expectedBase); + + // Clean up + QFile::remove(saveFile); +} + #include "UnitTest.h" UT_REGISTER_TEST(PlanMasterControllerTest, TestLabel::Integration, TestLabel::MissionManager) diff --git a/test/MissionManager/PlanMasterControllerTest.h b/test/MissionManager/PlanMasterControllerTest.h index cc1f5fcefbf7..983247d24371 100644 --- a/test/MissionManager/PlanMasterControllerTest.h +++ b/test/MissionManager/PlanMasterControllerTest.h @@ -17,6 +17,17 @@ private slots: void _testDirtyFlagsMatrix_data(); void _testDirtyFlagsMatrix(); + // File name property tests + void _testFileNamesSetOnLoad(); + void _testCurrentPlanFileNameWritable(); + void _testPlanFileRenamed(); + void _testSaveWithCurrentName(); + void _testSaveWithCurrentNameNoFile(); + void _testResolvedPlanFileExists(); + void _testFileNamesClearedOnRemoveAll(); + void _testFileNamesClearedOnRemoveAllFromVehicle(); + void _testSaveUpdatesOriginalFileName(); + private: enum DirtyScenario { UploadPreservesSaveDirtyTrue, From 45c45f8a225d561fdc33ed7e8f37d20d571e4244 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 6 Mar 2026 13:54:27 -0800 Subject: [PATCH 036/119] Plan: add layer switcher control for Mission/GeoFence/Rally Add an expandable layer switching control anchored to the top-right of the map (left of the right panel). Only the active layer icon is shown; clicking it expands the other two choices leftward. Selecting a choice switches the editing layer, expands the corresponding tree view section, and collapses the picker. An auto-collapse timer and map-click dismiss are also included. New SVG icons added for GeoFence and RallyPoint layers. Changes: - PlanView.qml: layer switcher Item with expand/collapse animation - MissionItemTreeView.qml: public selectLayer() API - PlanViewRightPanel.qml: selectLayer() bridge that opens panel first - resources/GeoFence.svg, RallyPoint.svg: new layer icons - qgcresources.qrc: register new icons --- qgcresources.qrc | 2 + resources/GeoFence.svg | 8 ++ resources/RallyPoint.svg | 3 + src/QmlControls/MissionItemTreeView.qml | 27 ++++++ src/QmlControls/PlanView.qml | 107 ++++++++++++++++++++++++ src/QmlControls/PlanViewRightPanel.qml | 10 +++ 6 files changed, 157 insertions(+) create mode 100644 resources/GeoFence.svg create mode 100644 resources/RallyPoint.svg diff --git a/qgcresources.qrc b/qgcresources.qrc index 400c52a8c067..e16a29a5c49e 100644 --- a/qgcresources.qrc +++ b/qgcresources.qrc @@ -28,6 +28,7 @@ resources/gear-black.svg resources/gear-white.svg resources/GearWithPaperPlane.svg + resources/GeoFence.svg resources/helicoptericon.svg resources/JoystickBezel.png resources/JoystickBezelLight.png @@ -52,6 +53,7 @@ resources/QGCLogoWhite.svg resources/QGCLogoArrow.svg resources/QGroundControlConnect.svg + resources/RallyPoint.svg resources/rtl.svg resources/sliders.svg resources/SaveToDisk.svg diff --git a/resources/GeoFence.svg b/resources/GeoFence.svg new file mode 100644 index 000000000000..dcc9c279f412 --- /dev/null +++ b/resources/GeoFence.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/RallyPoint.svg b/resources/RallyPoint.svg new file mode 100644 index 000000000000..34498b3b17b2 --- /dev/null +++ b/resources/RallyPoint.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/QmlControls/MissionItemTreeView.qml b/src/QmlControls/MissionItemTreeView.qml index 370c4a58331e..7d199ed27ac0 100644 --- a/src/QmlControls/MissionItemTreeView.qml +++ b/src/QmlControls/MissionItemTreeView.qml @@ -57,6 +57,33 @@ TreeView { } } + // Public API: select a layer and expand its group (no toggle — always selects). + function selectLayer(nodeType) { + root.collapseRecursively() + + let targetRow = -1 + switch (nodeType) { + case "missionGroup": + targetRow = _rowMission + _editingLayer = _layerMission + break + case "fenceGroup": + targetRow = _rowFence + _editingLayer = _layerFence + break + case "rallyGroup": + targetRow = _rowRally + _editingLayer = _layerRally + break + } + + if (targetRow >= 0) { + root.expand(targetRow) + root.forceLayout() + root.positionViewAtRow(targetRow, Qt.AlignTop) + } + } + // Switching editing layer on group expand — exclusive: only one group expanded at a time. // Clicking an already-expanded group collapses it (toggle behavior). function _expandExclusive(clickedNodeType) { diff --git a/src/QmlControls/PlanView.qml b/src/QmlControls/PlanView.qml index 3b9abd247c8b..db7cbd70bea7 100644 --- a/src/QmlControls/PlanView.qml +++ b/src/QmlControls/PlanView.qml @@ -257,6 +257,11 @@ Item { onMapClicked: (mouse) => { // Take focus to close any previous editing editorMap.focus = true + + // Collapse layer switcher on any map click + layerSwitcher.expanded = false + collapseTimer.stop() + if (!mainWindow.allowViewSwitch()) { return } @@ -496,6 +501,108 @@ Item { editorMap: editorMap } + // Layer switching icons — only active icon visible; click to expand choices leftward + Item { + id: layerSwitcher + anchors.right: rightPanel.left + anchors.rightMargin: _toolsMargin + anchors.top: parent.top + anchors.topMargin: _toolsMargin + width: layerRow.width + height: _layerButtonSize + z: QGroundControl.zOrderWidgets + + property bool expanded: false + property real _layerButtonSize: ScreenTools.defaultFontPixelHeight * 2.0 + property real _spacing: ScreenTools.defaultFontPixelHeight * 0.25 + + readonly property var _layers: [ + { layer: _layerMission, icon: "/res/waypoint.svg", nodeType: "missionGroup" }, + { layer: _layerFence, icon: "/res/GeoFence.svg", nodeType: "fenceGroup" }, + { layer: _layerRally, icon: "/res/RallyPoint.svg", nodeType: "rallyGroup" } + ] + + Timer { + id: collapseTimer + interval: 5000 + onTriggered: layerSwitcher.expanded = false + } + + function toggle() { + expanded = !expanded + if (expanded) { + collapseTimer.restart() + } else { + collapseTimer.stop() + } + } + + function choose(nodeType) { + expanded = false + collapseTimer.stop() + rightPanel.selectLayer(nodeType) + } + + // Row laid out right-to-left: active icon on the right, choices expand left + Row { + id: layerRow + anchors.right: parent.right + spacing: layerSwitcher._spacing + layoutDirection: Qt.RightToLeft + + // Active layer button (always visible) + Rectangle { + width: layerSwitcher._layerButtonSize + height: width + radius: ScreenTools.defaultBorderRadius + color: QGroundControl.globalPalette.buttonHighlight + + QGCColoredImage { + anchors.centerIn: parent + width: parent.width * 0.6 + height: width + source: layerSwitcher._layers.find(l => l.layer === _editingLayer)?.icon ?? "/res/waypoint.svg" + color: QGroundControl.globalPalette.buttonHighlightText + } + + MouseArea { + anchors.fill: parent + onClicked: layerSwitcher.toggle() + } + } + + // Choice buttons (only layers that are NOT the current one) + Repeater { + model: layerSwitcher._layers.filter(l => l.layer !== _editingLayer) + + Rectangle { + required property var modelData + width: layerSwitcher._layerButtonSize + height: width + radius: ScreenTools.defaultBorderRadius + color: QGroundControl.globalPalette.button + visible: opacity > 0 + opacity: layerSwitcher.expanded ? 1 : 0 + + Behavior on opacity { NumberAnimation { duration: 150 } } + + QGCColoredImage { + anchors.centerIn: parent + width: parent.width * 0.6 + height: width + source: modelData.icon + color: QGroundControl.globalPalette.buttonText + } + + MouseArea { + anchors.fill: parent + onClicked: layerSwitcher.choose(modelData.nodeType) + } + } + } + } + } + RowLayout { id: missionStatus anchors.margins: _toolsMargin diff --git a/src/QmlControls/PlanViewRightPanel.qml b/src/QmlControls/PlanViewRightPanel.qml index 1bd7056621d2..b85d33951e2d 100644 --- a/src/QmlControls/PlanViewRightPanel.qml +++ b/src/QmlControls/PlanViewRightPanel.qml @@ -89,9 +89,19 @@ Item { } MissionItemTreeView { + id: missionItemTreeView anchors.fill: parent editorMap: root.editorMap planMasterController: root.planMasterController } } + + function selectLayer(nodeType) { + // Ensure panel is open + if (!panelOpenCloseButton._expanded) { + root.anchors.left = undefined + root.anchors.right = root.parent.right + } + missionItemTreeView.selectLayer(nodeType) + } } From a9d8dc0225eeb7c6f84991677099756bff894c86 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 6 Mar 2026 13:54:27 -0800 Subject: [PATCH 037/119] Plan: add layer switcher control for Mission/GeoFence/Rally Add an expandable layer switching control anchored to the top-right of the map (left of the right panel). Only the active layer icon is shown; clicking it expands the other two choices leftward. Selecting a choice switches the editing layer, expands the corresponding tree view section, and collapses the picker. An auto-collapse timer and map-click dismiss are also included. New SVG icons added for GeoFence and RallyPoint layers. Changes: - PlanView.qml: layer switcher Item with expand/collapse animation - MissionItemTreeView.qml: public selectLayer() API - PlanViewRightPanel.qml: selectLayer() bridge that opens panel first - resources/GeoFence.svg, RallyPoint.svg: new layer icons - qgcresources.qrc: register new icons --- src/QmlControls/MissionItemTreeView.qml | 21 ++++++++++----------- src/QmlControls/PlanView.qml | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/QmlControls/MissionItemTreeView.qml b/src/QmlControls/MissionItemTreeView.qml index 7d199ed27ac0..f23e7181399b 100644 --- a/src/QmlControls/MissionItemTreeView.qml +++ b/src/QmlControls/MissionItemTreeView.qml @@ -26,6 +26,13 @@ TreeView { selectionBehavior: TableView.SelectionDisabled rowSpacing: 2 + // After collapseRecursively() groups are always at these row indices + readonly property int _rowPlanFile: 0 + readonly property int _rowDefaults: 1 + readonly property int _rowMission: 2 + readonly property int _rowFence: 3 + readonly property int _rowRally: 4 + // QGCFlickableScrollIndicator expects parent to have indicatorColor (provided by QGCFlickable/QGCListView) property color indicatorColor: qgcPal.text @@ -34,14 +41,6 @@ TreeView { QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Horizontal } QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Vertical } - // After full collapse, top-level group rows are: - // 0 = Plan File, 1 = Defaults, 2 = Mission Items, 3 = GeoFence, 4 = Rally Points - readonly property int _rowPlanFile: 0 - readonly property int _rowDefaults: 1 - readonly property int _rowMission: 2 - readonly property int _rowFence: 3 - readonly property int _rowRally: 4 - Component.onCompleted: { // Expand only Mission Items by default root.expand(_rowMission) @@ -80,7 +79,7 @@ TreeView { if (targetRow >= 0) { root.expand(targetRow) root.forceLayout() - root.positionViewAtRow(targetRow, Qt.AlignTop) + root.positionViewAtRow(targetRow, TableView.AlignTop) } } @@ -117,7 +116,7 @@ TreeView { if (alreadyActive) { _editingLayer = -1 root.forceLayout() - root.positionViewAtRow(0, Qt.AlignTop) + root.positionViewAtRow(0, TableView.AlignTop) return } @@ -151,7 +150,7 @@ TreeView { // After collapse/expand the view may still be scrolled past the new content. // Force layout then scroll so the expanded group header is at the top. root.forceLayout() - root.positionViewAtRow(targetRow, Qt.AlignTop) + root.positionViewAtRow(targetRow, TableView.AlignTop) } } diff --git a/src/QmlControls/PlanView.qml b/src/QmlControls/PlanView.qml index db7cbd70bea7..ac9db0c581e3 100644 --- a/src/QmlControls/PlanView.qml +++ b/src/QmlControls/PlanView.qml @@ -565,7 +565,7 @@ Item { color: QGroundControl.globalPalette.buttonHighlightText } - MouseArea { + QGCMouseArea { anchors.fill: parent onClicked: layerSwitcher.toggle() } @@ -594,7 +594,7 @@ Item { color: QGroundControl.globalPalette.buttonText } - MouseArea { + QGCMouseArea { anchors.fill: parent onClicked: layerSwitcher.choose(modelData.nodeType) } From be4f556ac7f0a855de997fd25b5eb07a8340c19b Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 6 Mar 2026 15:29:48 -0800 Subject: [PATCH 038/119] Enforce log hygiene in all unit tests Add automatic detection of uncategorized and critical log messages to the base UnitTest::cleanup(). Every test now fails if it produces: - Uncategorized messages (raw qWarning/qDebug without a logging category) - Unexpected qCCritical messages Tests that intentionally trigger error paths can declare expected messages via the new expectLogMessage(QtMsgType, QRegularExpression) API. Source fixes uncovered by the new enforcement: - QGCMapPolygon/QGCMapPolyline: convert qWarning to qCWarning with new logging categories; remove runningUnitTests() guards - PX4FirmwarePlugin: convert qWarning/qDebug to qCWarning/qCDebug with new PX4FirmwarePluginLog category - StateMachineProfiler: fix printf-style format strings that were incompatible with QString::arg() (caused 'Argument missing' warnings) - OsmParserThread: pass nullptr to QObject base instead of parent, since moveToThread() on a parented QObject is forbidden by Qt - OsmParser: add destructor to properly delete the worker thread object Test fixes: - InitialConnectTest: remove redundant init/cleanup overrides (now in base class) - State machine tests, decompression tests, tile cache test: declare expected critical/warning messages for intentional error-path testing --- src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc | 18 ++++--- src/QmlControls/QGCMapPolygon.cc | 17 ++++--- src/QmlControls/QGCMapPolyline.cc | 14 ++--- .../Helpers/StateMachineProfiler.cc | 23 +++++---- src/Viewer3D/Providers/Osm/OsmParser.cc | 10 ++++ src/Viewer3D/Providers/Osm/OsmParser.h | 1 + src/Viewer3D/Providers/Osm/OsmParserThread.cc | 4 +- .../QGCTileCacheDatabaseTest.cc | 4 ++ test/UnitTestFramework/UnitTest.cc | 51 +++++++++++++++++++ test/UnitTestFramework/UnitTest.h | 12 +++++ .../QGCStreamingDecompressionTest.cc | 3 ++ .../StateMachine/QGCStateMachineTest.cc | 15 ++++++ .../states/AsyncFunctionStateTest.cc | 8 +++ .../states/SendMavlinkCommandStateTest.cc | 6 +++ test/Vehicle/InitialConnectTest.cc | 45 ---------------- test/Vehicle/InitialConnectTest.h | 2 - 16 files changed, 152 insertions(+), 81 deletions(-) diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc index 96ab48cb1052..c7320834d6ca 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc @@ -2,16 +2,18 @@ #include "PX4ParameterMetaData.h" #include "QGCApplication.h" #include "PX4AutoPilotPlugin.h" +#include "QGCLoggingCategory.h" #include "SettingsManager.h" #include "PlanViewSettings.h" #include "ParameterManager.h" #include "Vehicle.h" -#include #include #include "px4_custom_mode.h" +QGC_LOGGING_CATEGORY(PX4FirmwarePluginLog, "FirmwarePlugin.PX4FirmwarePlugin") + PX4FirmwarePluginInstanceData::PX4FirmwarePluginInstanceData(QObject* parent) : FirmwarePluginInstanceData(parent) , versionNotified(false) @@ -141,7 +143,7 @@ bool PX4FirmwarePlugin::setFlightMode(const QString& flightMode, uint8_t* base_m } if (!found) { - qWarning() << "Unknown flight Mode" << flightMode; + qCWarning(PX4FirmwarePluginLog) << "Unknown flight Mode" << flightMode; } return found; @@ -182,7 +184,7 @@ FactMetaData* PX4FirmwarePlugin::_getMetaDataForFact(QObject* parameterMetaData, if (px4MetaData) { return px4MetaData->getMetaDataForFact(name, vehicleType, type); } else { - qWarning() << "Internal error: pointer passed to PX4FirmwarePlugin::getMetaDataForFact not PX4ParameterMetaData"; + qCWarning(PX4FirmwarePluginLog) << "Internal error: pointer passed to PX4FirmwarePlugin::getMetaDataForFact not PX4ParameterMetaData"; } return nullptr; @@ -260,7 +262,7 @@ QString PX4FirmwarePlugin::missionCommandOverrides(QGCMAVLink::VehicleClass_t ve case QGCMAVLink::VehicleClassRoverBoat: return QStringLiteral(":/json/PX4-MavCmdInfoRover.json"); default: - qWarning() << "PX4FirmwarePlugin::missionCommandOverrides called with bad VehicleClass_t:" << vehicleClass; + qCWarning(PX4FirmwarePluginLog) << "PX4FirmwarePlugin::missionCommandOverrides called with bad VehicleClass_t:" << vehicleClass; return QString(); } } @@ -305,7 +307,7 @@ void PX4FirmwarePlugin::_mavCommandResult(int vehicleId, int component, int comm auto* vehicle = qobject_cast(sender()); if (!vehicle) { - qWarning() << "Dynamic cast failed!"; + qCWarning(PX4FirmwarePluginLog) << "Dynamic cast failed!"; return; } @@ -433,13 +435,13 @@ static void _pauseVehicleThenChangeAltResultHandler(void* resultHandlerData, int if (ack.result != MAV_RESULT_ACCEPTED) { switch (failureCode) { case Vehicle::MavCmdResultCommandResultOnly: - qDebug() << QStringLiteral("MAV_CMD_DO_REPOSITION error(%1)").arg(ack.result); + qCDebug(PX4FirmwarePluginLog) << QStringLiteral("MAV_CMD_DO_REPOSITION error(%1)").arg(ack.result); break; case Vehicle::MavCmdResultFailureNoResponseToCommand: - qDebug() << "MAV_CMD_DO_REPOSITION no response from vehicle"; + qCDebug(PX4FirmwarePluginLog) << "MAV_CMD_DO_REPOSITION no response from vehicle"; break; case Vehicle::MavCmdResultFailureDuplicateCommand: - qDebug() << "Internal Error: MAV_CMD_DO_REPOSITION could not be sent due to duplicate command"; + qCDebug(PX4FirmwarePluginLog) << "Internal Error: MAV_CMD_DO_REPOSITION could not be sent due to duplicate command"; break; } } diff --git a/src/QmlControls/QGCMapPolygon.cc b/src/QmlControls/QGCMapPolygon.cc index 8b0c9b49fda7..235320ff283c 100644 --- a/src/QmlControls/QGCMapPolygon.cc +++ b/src/QmlControls/QGCMapPolygon.cc @@ -4,12 +4,15 @@ #include "JsonParsing.h" #include "QGCQGeoCoordinate.h" #include "QGCApplication.h" +#include "QGCLoggingCategory.h" #include "ShapeFileHelper.h" #include "KMLDomDocument.h" #include #include +QGC_LOGGING_CATEGORY(QGCMapPolygonLog, "QMLControls.QGCMapPolygon") + QGCMapPolygon::QGCMapPolygon(QObject* parent) : QObject (parent) , _dirty (false) @@ -311,8 +314,8 @@ void QGCMapPolygon::_polygonModelDirtyChanged(bool dirty) void QGCMapPolygon::removeVertex(int vertexIndex) { - if (vertexIndex < 0 && vertexIndex > _polygonPath.length() - 1) { - qWarning() << "Call to removePolygonCoordinate with bad vertexIndex:count" << vertexIndex << _polygonPath.length(); + if (vertexIndex < 0 || vertexIndex >= _polygonPath.length()) { + qCWarning(QGCMapPolygonLog) << "Call to removePolygonCoordinate with bad vertexIndex:count" << vertexIndex << _polygonPath.length(); return; } @@ -420,7 +423,7 @@ QGeoCoordinate QGCMapPolygon::vertexCoordinate(int vertex) const if (vertex >= 0 && vertex < _polygonPath.count()) { return _polygonPath[vertex].value(); } else { - qWarning() << "QGCMapPolygon::vertexCoordinate bad vertex requested:count" << vertex << _polygonPath.count(); + qCWarning(QGCMapPolygonLog) << "QGCMapPolygon::vertexCoordinate bad vertex requested:count" << vertex << _polygonPath.count(); return QGeoCoordinate(); } } @@ -487,7 +490,7 @@ void QGCMapPolygon::offset(double distance) auto intersect = rgOffsetEdges[prevIndex].intersects(rgOffsetEdges[i], &newVertex); if (intersect == QLineF::NoIntersection) { // FIXME: Better error handling? - qWarning("Intersection failed"); + qCWarning(QGCMapPolygonLog, "Intersection failed"); return; } QGeoCoordinate coord; @@ -649,10 +652,8 @@ void QGCMapPolygon::selectVertex(int index) if(-1 <= index && index < count()) { _selectedVertexIndex = index; } else { - if (!qgcApp()->runningUnitTests()) { - qWarning() << QString("QGCMapPolygon: Selected vertex index (%1) is out of bounds! " - "Polygon vertices indexes range is [%2..%3].").arg(index).arg(0).arg(count()-1); - } + qCWarning(QGCMapPolygonLog) << QString("QGCMapPolygon: Selected vertex index (%1) is out of bounds! " + "Polygon vertices indexes range is [%2..%3].").arg(index).arg(0).arg(count()-1); _selectedVertexIndex = -1; // deselect vertex } diff --git a/src/QmlControls/QGCMapPolyline.cc b/src/QmlControls/QGCMapPolyline.cc index ea1c632be9e4..d91db82d0bef 100644 --- a/src/QmlControls/QGCMapPolyline.cc +++ b/src/QmlControls/QGCMapPolyline.cc @@ -4,12 +4,14 @@ #include "JsonParsing.h" #include "QGCQGeoCoordinate.h" #include "QGCApplication.h" -#include "ShapeFileHelper.h" #include "QGCLoggingCategory.h" +#include "ShapeFileHelper.h" #include #include +QGC_LOGGING_CATEGORY(QGCMapPolylineLog, "QMLControls.QGCMapPolyline") + QGCMapPolyline::QGCMapPolyline(QObject* parent) : QObject (parent) , _dirty (false) @@ -231,7 +233,7 @@ void QGCMapPolyline::appendVertex(const QGeoCoordinate& coordinate) void QGCMapPolyline::removeVertex(int vertexIndex) { if (vertexIndex < 0 || vertexIndex > _polylinePath.length() - 1) { - qWarning() << "Call to removeVertex with bad vertexIndex:count" << vertexIndex << _polylinePath.length(); + qCWarning(QGCMapPolylineLog) << "Call to removeVertex with bad vertexIndex:count" << vertexIndex << _polylinePath.length(); return; } @@ -265,7 +267,7 @@ QGeoCoordinate QGCMapPolyline::vertexCoordinate(int vertex) const if (vertex >= 0 && vertex < _polylinePath.count()) { return _polylinePath[vertex].value(); } else { - qWarning() << "QGCMapPolyline::vertexCoordinate bad vertex requested"; + qCWarning(QGCMapPolylineLog) << "QGCMapPolyline::vertexCoordinate bad vertex requested"; return QGeoCoordinate(); } } @@ -439,10 +441,8 @@ void QGCMapPolyline::selectVertex(int index) if(-1 <= index && index < count()) { _selectedVertexIndex = index; } else { - if (!qgcApp()->runningUnitTests()) { - qWarning() << QStringLiteral("QGCMapPolyline: Selected vertex index (%1) is out of bounds! " - "Polyline vertices indexes range is [%2..%3].").arg(index).arg(0).arg(count()-1); - } + qCWarning(QGCMapPolylineLog) << QStringLiteral("QGCMapPolyline: Selected vertex index (%1) is out of bounds! " + "Polyline vertices indexes range is [%2..%3].").arg(index).arg(0).arg(count()-1); _selectedVertexIndex = -1; // deselect vertex } diff --git a/src/Utilities/StateMachine/Helpers/StateMachineProfiler.cc b/src/Utilities/StateMachine/Helpers/StateMachineProfiler.cc index a8f18f372f33..9aa1b8216354 100644 --- a/src/Utilities/StateMachine/Helpers/StateMachineProfiler.cc +++ b/src/Utilities/StateMachine/Helpers/StateMachineProfiler.cc @@ -67,18 +67,23 @@ QString StateMachineProfiler::summary() const return a.totalTimeMs > b.totalTimeMs; }); - lines << QStringLiteral("%-30s %8s %10s %10s %10s %10s") - .arg("State", "Count", "Total(ms)", "Avg(ms)", "Min(ms)", "Max(ms)"); + lines << QStringLiteral("%1 %2 %3 %4 %5 %6") + .arg("State", -30) + .arg("Count", 8) + .arg("Total(ms)", 10) + .arg("Avg(ms)", 10) + .arg("Min(ms)", 10) + .arg("Max(ms)", 10); lines << QString(80, '-'); for (const auto& p : sortedProfiles) { - lines << QStringLiteral("%-30s %8d %10lld %10.1f %10lld %10lld") - .arg(p.name.left(30)) - .arg(p.entryCount) - .arg(p.totalTimeMs) - .arg(p.averageTimeMs()) - .arg(p.minTimeMs == std::numeric_limits::max() ? 0 : p.minTimeMs) - .arg(p.maxTimeMs); + lines << QStringLiteral("%1 %2 %3 %4 %5 %6") + .arg(p.name.left(30), -30) + .arg(p.entryCount, 8) + .arg(p.totalTimeMs, 10) + .arg(p.averageTimeMs(), 10, 'f', 1) + .arg(p.minTimeMs == std::numeric_limits::max() ? 0 : p.minTimeMs, 10) + .arg(p.maxTimeMs, 10); } return lines.join('\n'); diff --git a/src/Viewer3D/Providers/Osm/OsmParser.cc b/src/Viewer3D/Providers/Osm/OsmParser.cc index 10171d8c09cf..6743aaed1483 100644 --- a/src/Viewer3D/Providers/Osm/OsmParser.cc +++ b/src/Viewer3D/Providers/Osm/OsmParser.cc @@ -21,6 +21,16 @@ OsmParser::OsmParser(QObject *parent) connect(_osmParserWorker, &OsmParserThread::fileParsed, this, &OsmParser::_onOsmParserFinished); } +OsmParser::~OsmParser() +{ + // Stop the worker thread's event loop and wait for it to finish. + // Once the thread is joined, no events are being processed and + // the destructor chain is safe to run from our thread. + _osmParserWorker->thread()->quit(); + _osmParserWorker->thread()->wait(); + delete _osmParserWorker; +} + void OsmParser::setGpsRef(const QGeoCoordinate &gpsRef) { _gpsRefPoint = gpsRef; diff --git a/src/Viewer3D/Providers/Osm/OsmParser.h b/src/Viewer3D/Providers/Osm/OsmParser.h index e562f64126dc..30fbcbb73f95 100644 --- a/src/Viewer3D/Providers/Osm/OsmParser.h +++ b/src/Viewer3D/Providers/Osm/OsmParser.h @@ -22,6 +22,7 @@ class OsmParser : public Viewer3DMapProvider public: explicit OsmParser(QObject *parent = nullptr); + ~OsmParser() override; bool mapLoaded() const override { return _mapLoadedFlag; } QGeoCoordinate gpsRef() const override { return _gpsRefPoint; } diff --git a/src/Viewer3D/Providers/Osm/OsmParserThread.cc b/src/Viewer3D/Providers/Osm/OsmParserThread.cc index 28772af2b0cb..ca63c629a52e 100644 --- a/src/Viewer3D/Providers/Osm/OsmParserThread.cc +++ b/src/Viewer3D/Providers/Osm/OsmParserThread.cc @@ -214,8 +214,8 @@ void OsmParserThread::BuildingType_t::append(const std::vector &newPo // OsmParserThread // ============================================================================ -OsmParserThread::OsmParserThread(QObject *parent) - : QObject{parent} +OsmParserThread::OsmParserThread(QObject * /*parent*/) + : QObject{nullptr} , _workerThread(new QThread()) { connect(this, &OsmParserThread::startThread, this, &OsmParserThread::_parseOsmFile); diff --git a/test/QtLocationPlugin/QGCTileCacheDatabaseTest.cc b/test/QtLocationPlugin/QGCTileCacheDatabaseTest.cc index 56f9d84dd49a..36515a754425 100644 --- a/test/QtLocationPlugin/QGCTileCacheDatabaseTest.cc +++ b/test/QtLocationPlugin/QGCTileCacheDatabaseTest.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -59,6 +60,9 @@ void QGCTileCacheDatabaseTest::_testInitWithValidPath() void QGCTileCacheDatabaseTest::_testInitWithEmptyPath() { + // Expected: critical about missing cache directory + expectLogMessage(QtCriticalMsg, QRegularExpression("Could not find suitable cache directory")); + const QString emptyPath; QGCTileCacheDatabase db(emptyPath); QVERIFY(!db.init()); diff --git a/test/UnitTestFramework/UnitTest.cc b/test/UnitTestFramework/UnitTest.cc index 96f7820751ad..006df0227537 100644 --- a/test/UnitTestFramework/UnitTest.cc +++ b/test/UnitTestFramework/UnitTest.cc @@ -668,10 +668,16 @@ void UnitTest::cleanupTestCase() } } +void UnitTest::expectLogMessage(QtMsgType type, const QRegularExpression &pattern) +{ + _expectedLogMessages.append({type, pattern}); +} + void UnitTest::init() { _initCalled = true; _failureContextDumped = false; + _expectedLogMessages.clear(); // Start capturing log messages for this test (cleared from previous test) QGCLogging::clearCapturedMessages(); @@ -697,6 +703,51 @@ void UnitTest::cleanup() // Process any lingering events to prevent cross-test contamination settleEventLoopForCleanup(3, 0); + + // Fail the test if any uncategorized or critical log messages were captured. + // Skip if the test already failed to avoid noisy double-failure reports. + if (!QTest::currentTestFailed()) { + QString uncategorizedDetails; + QString criticalDetails; + + auto isExpected = [this](const CapturedLogMessage &m) { + for (const auto &e : _expectedLogMessages) { + if (e.type == m.type && e.pattern.match(m.message).hasMatch()) { + return true; + } + } + return false; + }; + + const auto allMsgs = QGCLogging::capturedMessages(); + for (const auto &m : allMsgs) { + if (isExpected(m)) { + continue; + } + if (m.category.isEmpty() || m.category == QStringLiteral("default")) { + const char *level = (m.type == QtDebugMsg) ? "debug" + : (m.type == QtWarningMsg) ? "warning" + : (m.type == QtInfoMsg) ? "info" + : "other"; + uncategorizedDetails += QStringLiteral(" [%1] %2\n").arg(QLatin1String(level), m.message); + } + if (m.type == QtCriticalMsg) { + criticalDetails += QStringLiteral(" [%1] %2\n").arg(m.category, m.message); + } + } + + if (!uncategorizedDetails.isEmpty() || !criticalDetails.isEmpty()) { + QString msg; + if (!uncategorizedDetails.isEmpty()) { + msg += QStringLiteral("Uncategorized log messages (use qCDebug/qCWarning with a category):\n%1") + .arg(uncategorizedDetails); + } + if (!criticalDetails.isEmpty()) { + msg += QStringLiteral("Critical log messages:\n%1").arg(criticalDetails); + } + QFAIL(qPrintable(msg)); + } + } } void UnitTest::dumpFailureContextIfTestFailed(QStringView reason) diff --git a/test/UnitTestFramework/UnitTest.h b/test/UnitTestFramework/UnitTest.h index b47e1f0ade88..42f233c0a10f 100644 --- a/test/UnitTestFramework/UnitTest.h +++ b/test/UnitTestFramework/UnitTest.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -436,7 +437,17 @@ protected slots: /// Allows derived fixtures to append state to failure dumps. virtual QString failureContextSummary() const; + /// Declare that a captured log message matching @a pattern at level @a type + /// is expected and should not cause a test failure in cleanup(). + /// Call this before the code that emits the message. + void expectLogMessage(QtMsgType type, const QRegularExpression &pattern); + private: + struct ExpectedLogMessage { + QtMsgType type; + QRegularExpression pattern; + }; + void _cleanupTempFiles(); void _resetTestState(); @@ -445,6 +456,7 @@ protected slots: QList _tempFiles; QList _tempDirs; + QList _expectedLogMessages; TestLabels _labels; bool _unitTestRun = false; diff --git a/test/Utilities/Compression/QGCStreamingDecompressionTest.cc b/test/Utilities/Compression/QGCStreamingDecompressionTest.cc index bfe8cafab980..73167f9043b9 100644 --- a/test/Utilities/Compression/QGCStreamingDecompressionTest.cc +++ b/test/Utilities/Compression/QGCStreamingDecompressionTest.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "QGCArchiveFile.h" @@ -123,6 +124,7 @@ void QGCStreamingDecompressionTest::_testDecompressDeviceErrors() // Test writeData returns -1 QGCDecompressDevice device4(":/unittest/manifest.json.gz"); QVERIFY(device4.open(QIODevice::ReadOnly)); + expectLogMessage(QtWarningMsg, QRegularExpression("ReadOnly device")); QCOMPARE(device4.write("test", 4), qint64(-1)); device4.close(); } @@ -238,6 +240,7 @@ void QGCStreamingDecompressionTest::_testArchiveFileErrors() // Test writeData returns -1 QGCArchiveFile device4(":/unittest/manifest.json.zip", "manifest.json"); QVERIFY(device4.open(QIODevice::ReadOnly)); + expectLogMessage(QtWarningMsg, QRegularExpression("ReadOnly device")); QCOMPARE(device4.write("test", 4), qint64(-1)); device4.close(); } diff --git a/test/Utilities/StateMachine/QGCStateMachineTest.cc b/test/Utilities/StateMachine/QGCStateMachineTest.cc index 62c59059b554..dbc8afc8a589 100644 --- a/test/Utilities/StateMachine/QGCStateMachineTest.cc +++ b/test/Utilities/StateMachine/QGCStateMachineTest.cc @@ -4,6 +4,7 @@ #include "QGCStateMachine.h" #include "WaitStateBase.h" +#include #include #include #include @@ -75,6 +76,9 @@ void QGCStateMachineTest::_testGlobalErrorState() }); machine.setGlobalErrorState(errorState); + // The async state has no completion connection and no timeout — expected critical + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + auto* asyncState = machine.addAsyncFunctionState(QStringLiteral("Async"), [](AsyncFunctionState* state) { QTimer::singleShot(50, state, [state]() { state->fail(); }); }); @@ -141,6 +145,9 @@ void QGCStateMachineTest::_testLocalErrorState() localErrorHandled = true; }); + // The async state has no completion connection and no timeout — expected critical + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + auto* asyncState = new AsyncFunctionState(QStringLiteral("Async"), &machine, [](AsyncFunctionState* state) { QTimer::singleShot(50, state, [state]() { state->fail(); }); }); @@ -319,6 +326,10 @@ void QGCStateMachineTest::_testErrorHandlerFactories() auto* errorState = machine.addLogAndContinueErrorState(QStringLiteral("ErrorContinue"), finalState); machine.setGlobalErrorState(errorState); + // Expected: async state critical (no completion connection) + error handler warning + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + expectLogMessage(QtWarningMsg, QRegularExpression("error handled in")); + auto* failingState = machine.addAsyncFunctionState(QStringLiteral("Failing"), [](AsyncFunctionState* state) { QTimer::singleShot(0, state, [state]() { state->fail(); }); }); @@ -336,6 +347,10 @@ void QGCStateMachineTest::_testErrorHandlerFactories() auto* errorState = machine.addLogAndStopErrorState(QStringLiteral("ErrorStop")); machine.setGlobalErrorState(errorState); + // Expected: async state critical (no completion connection) + error handler warning + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + expectLogMessage(QtWarningMsg, QRegularExpression("stopping due to error in")); + auto* failingState = machine.addAsyncFunctionState(QStringLiteral("Failing"), [](AsyncFunctionState* state) { QTimer::singleShot(0, state, [state]() { state->fail(); }); }); diff --git a/test/Utilities/StateMachine/states/AsyncFunctionStateTest.cc b/test/Utilities/StateMachine/states/AsyncFunctionStateTest.cc index ed5e259716a8..a69bfef2da11 100644 --- a/test/Utilities/StateMachine/states/AsyncFunctionStateTest.cc +++ b/test/Utilities/StateMachine/states/AsyncFunctionStateTest.cc @@ -1,6 +1,8 @@ #include "AsyncFunctionStateTest.h" #include "StateTestCommon.h" +#include + void AsyncFunctionStateTest::_testAsyncFunctionState() { @@ -8,6 +10,9 @@ void AsyncFunctionStateTest::_testAsyncFunctionState() bool setupCalled = false; AsyncFunctionState* capturedState = nullptr; + // Expected: async state has no completion connection and no timeout + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + auto* asyncState = new AsyncFunctionState( QStringLiteral("TestAsync"), &machine, @@ -76,6 +81,9 @@ void AsyncFunctionStateTest::_testErrorTransition() QStateMachine machine; bool errorHandled = false; + // Expected: async state has no completion connection and no timeout + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + auto* asyncState = new AsyncFunctionState( QStringLiteral("TestError"), &machine, diff --git a/test/Utilities/StateMachine/states/SendMavlinkCommandStateTest.cc b/test/Utilities/StateMachine/states/SendMavlinkCommandStateTest.cc index 844e4f0d562e..dd9c2d8c0e33 100644 --- a/test/Utilities/StateMachine/states/SendMavlinkCommandStateTest.cc +++ b/test/Utilities/StateMachine/states/SendMavlinkCommandStateTest.cc @@ -1,6 +1,8 @@ #include "SendMavlinkCommandStateTest.h" #include "StateTestCommon.h" +#include + #include "SendMavlinkCommandState.h" @@ -40,6 +42,10 @@ void SendMavlinkCommandStateTest::_testUnconfiguredStateFails() { QStateMachine machine; + // Expected: unconfigured state emits critical + Qt warns about null connect + expectLogMessage(QtWarningMsg, QRegularExpression("invalid nullptr parameter")); + expectLogMessage(QtCriticalMsg, QRegularExpression("SendMavlinkCommandState not configured")); + // Create without configuration auto* state = new SendMavlinkCommandState(&machine); auto* errorState = new FunctionState(QStringLiteral("Error"), &machine, []() {}); diff --git a/test/Vehicle/InitialConnectTest.cc b/test/Vehicle/InitialConnectTest.cc index 878ab174a87e..781aaf0e49cf 100644 --- a/test/Vehicle/InitialConnectTest.cc +++ b/test/Vehicle/InitialConnectTest.cc @@ -13,7 +13,6 @@ #include "MockLink.h" #include "MockLinkMissionItemHandler.h" #include "MissionManager.h" -#include "QGCLogging.h" #include "ParameterManager.h" #include "RallyPointManager.h" #include "StandardModes.h" @@ -23,50 +22,6 @@ #include -void InitialConnectTest::init() -{ - VehicleTestManualConnect::init(); -} - -void InitialConnectTest::cleanup() -{ - // Disable capture first so no new messages arrive while we analyze - QGCLogging::setCaptureEnabled(false); - - QString uncategorizedDetails; - QString criticalDetails; - - const auto allMsgs = QGCLogging::capturedMessages(); - for (const auto &m : allMsgs) { - if (m.category.isEmpty() || m.category == QStringLiteral("default")) { - const char *level = (m.type == QtDebugMsg) ? "debug" - : (m.type == QtWarningMsg) ? "warning" - : (m.type == QtInfoMsg) ? "info" - : "other"; - uncategorizedDetails += QStringLiteral(" [%1] %2\n").arg(QLatin1String(level), m.message); - } - if (m.type == QtCriticalMsg) { - criticalDetails += QStringLiteral(" [%1] %2\n").arg(m.category, m.message); - } - } - - // Always perform parent cleanup for proper teardown - VehicleTestManualConnect::cleanup(); - - // Report failures after teardown so test infrastructure stays consistent - if (!uncategorizedDetails.isEmpty() || !criticalDetails.isEmpty()) { - QString msg; - if (!uncategorizedDetails.isEmpty()) { - msg += QStringLiteral("Uncategorized log messages (use qCDebug/qCWarning with a category):\n%1") - .arg(uncategorizedDetails); - } - if (!criticalDetails.isEmpty()) { - msg += QStringLiteral("Critical log messages:\n%1").arg(criticalDetails); - } - QFAIL(qPrintable(msg)); - } -} - void InitialConnectTest::_performTestCases_data() { QTest::addColumn("failureMode"); diff --git a/test/Vehicle/InitialConnectTest.h b/test/Vehicle/InitialConnectTest.h index da7ce809cab4..4f1764c6d351 100644 --- a/test/Vehicle/InitialConnectTest.h +++ b/test/Vehicle/InitialConnectTest.h @@ -7,8 +7,6 @@ class InitialConnectTest : public VehicleTestManualConnect Q_OBJECT private slots: - void init() override; - void cleanup() override; void _performTestCases_data(); void _performTestCases(); void _boardVendorProductId(); From 1aa096f145341917e4d38c5ba49ff2d5847b6433 Mon Sep 17 00:00:00 2001 From: alexklimaj Date: Thu, 5 Mar 2026 15:54:20 -0700 Subject: [PATCH 039/119] USBBoardInfo: add new ARK board entries and regex fallbacks --- src/Comms/USBBoardInfo.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Comms/USBBoardInfo.json b/src/Comms/USBBoardInfo.json index dc68a3ca6a92..3dccd3811a7a 100644 --- a/src/Comms/USBBoardInfo.json +++ b/src/Comms/USBBoardInfo.json @@ -35,6 +35,8 @@ { "vendorID": 12677, "productID": 57, "boardClass": "Pixhawk", "name": "ARK FMU V6X" }, { "vendorID": 12677, "productID": 58, "boardClass": "Pixhawk", "name": "ARK Pi6X" }, { "vendorID": 12677, "productID": 59, "boardClass": "Pixhawk", "name": "ARK FPV" }, + { "vendorID": 12677, "productID": 61, "boardClass": "Pixhawk", "name": "ARK FMU V6S" }, + { "vendorID": 12677, "productID": 62, "boardClass": "Pixhawk", "name": "ARK FMU V6XRT" }, { "vendorID": 1155, "productID": 22336, "boardClass": "Pixhawk", "name": "ArduPilot ChibiOS" }, { "vendorID": 4617, "productID": 22336, "boardClass": "Pixhawk", "name": "ArduPilot ChibiOS" }, @@ -118,6 +120,10 @@ { "regExp": "^ARK BL FPV.x$", "boardClass": "Pixhawk" }, { "regExp": "^ARK Pi6X.x$", "boardClass": "Pixhawk" }, { "regExp": "^ARK BL Pi6X.x$", "boardClass": "Pixhawk" }, + { "regExp": "^ARK FMU v6S$", "boardClass": "Pixhawk" }, + { "regExp": "^ARK BL FMU v6S$", "boardClass": "Pixhawk" }, + { "regExp": "^ARK FMU v6XRT$", "boardClass": "Pixhawk" }, + { "regExp": "^ARK BL FMU v6XRT$", "boardClass": "Pixhawk" }, { "regExp": "^PX4 TROPIC Community","boardClass": "Pixhawk" }, { "regExp": "^PX4 MR-TROPIC", "boardClass": "Pixhawk" }, { "regExp": "^PX4 BL MR-TROPIC", "boardClass": "Pixhawk" }, From 3fc3c33863247ded91d5a10e62b0641ec6d0d344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Tue, 23 Dec 2025 12:27:20 +0100 Subject: [PATCH 040/119] Make entryCoordinate explicit in VisualMissionItem Previously, VisualMissionItem::coordinate was implicitly treated as the entry coordinate. This was unsuitable for LandingComplexItem, where the main coordinate is the landing (exit), as it is the master/pivot for user editing and transformations. Because of this, setCoordinate operated on the landing point, while the coordinate getter still returned the final approach (entry). Introduced an explicit entryCoordinate alongside exitCoordinate, and redefined coordinate to alias either the entry or exit, depending on which is considered the main coordinate for a given VisualMissionItem subclass. For LandingComplexItem, coordinate and exitCoordinate now refer to the landing point, while the final approach is exposed via entryCoordinate. In addition, Qt signal wiring for these properties has been simplified by using connections set up during object construction, reducing manual signal emissions. --- src/MissionManager/LandingComplexItem.cc | 17 +++++---- src/MissionManager/LandingComplexItem.h | 3 +- src/MissionManager/MissionController.cc | 37 ++++++++++--------- src/MissionManager/MissionSettingsItem.cc | 4 +- src/MissionManager/MissionSettingsItem.h | 3 +- src/MissionManager/SimpleMissionItem.cc | 1 + src/MissionManager/SimpleMissionItem.h | 1 + .../StructureScanComplexItem.cc | 7 ++-- src/MissionManager/StructureScanComplexItem.h | 1 + .../TransectStyleComplexItem.cc | 12 +++--- src/MissionManager/TransectStyleComplexItem.h | 5 ++- src/MissionManager/VisualMissionItem.h | 5 ++- test/MissionManager/SimpleMissionItemTest.cc | 8 ++-- 13 files changed, 58 insertions(+), 46 deletions(-) diff --git a/src/MissionManager/LandingComplexItem.cc b/src/MissionManager/LandingComplexItem.cc index a84ba3b85fb8..0f6dff5034df 100644 --- a/src/MissionManager/LandingComplexItem.cc +++ b/src/MissionManager/LandingComplexItem.cc @@ -41,6 +41,12 @@ void LandingComplexItem::_init(void) connect(useLoiterToAlt(), &Fact::rawValueChanged, this, &LandingComplexItem::_recalcFromApproachModeChange); + connect(this, &LandingComplexItem::finalApproachCoordinateChanged,this, &LandingComplexItem::entryCoordinateChanged); + connect(this, &LandingComplexItem::landingCoordinateChanged, this, &LandingComplexItem::exitCoordinateChanged); + + // The main coordinate is aliased to the exit (landing point) because that's the core of this complex item and the master for all transformations + connect(this, &LandingComplexItem::exitCoordinateChanged, this, &LandingComplexItem::coordinateChanged); + connect(this, &LandingComplexItem::finalApproachCoordinateChanged,this, &LandingComplexItem::_recalcFromCoordinateChange); connect(this, &LandingComplexItem::landingCoordinateChanged, this, &LandingComplexItem::_recalcFromCoordinateChange); @@ -109,11 +115,9 @@ void LandingComplexItem::setLandingCoordinate(const QGeoCoordinate& coordinate) if (coordinate != _landingCoordinate) { _landingCoordinate = coordinate; if (_landingCoordSet) { - emit exitCoordinateChanged(coordinate); emit landingCoordinateChanged(coordinate); } else { _ignoreRecalcSignals = true; - emit exitCoordinateChanged(coordinate); emit landingCoordinateChanged(coordinate); _ignoreRecalcSignals = false; _landingCoordSet = true; @@ -127,7 +131,6 @@ void LandingComplexItem::setFinalApproachCoordinate(const QGeoCoordinate& coordi { if (coordinate != _finalApproachCoordinate) { _finalApproachCoordinate = coordinate; - emit coordinateChanged(coordinate); emit finalApproachCoordinateChanged(coordinate); } } @@ -176,7 +179,6 @@ void LandingComplexItem::_recalcFromHeadingAndDistanceChange(void) _ignoreRecalcSignals = true; emit slopeStartCoordinateChanged(_slopeStartCoordinate); emit finalApproachCoordinateChanged(_finalApproachCoordinate); - emit coordinateChanged(_finalApproachCoordinate); _calcGlideSlope(); _ignoreRecalcSignals = false; } @@ -219,7 +221,6 @@ void LandingComplexItem::_recalcFromRadiusChange(void) _ignoreRecalcSignals = true; emit finalApproachCoordinateChanged(_finalApproachCoordinate); - emit coordinateChanged(_finalApproachCoordinate); _ignoreRecalcSignals = false; } } @@ -252,7 +253,6 @@ void LandingComplexItem::_recalcFromApproachModeChange(void) _ignoreRecalcSignals = true; emit finalApproachCoordinateChanged(_finalApproachCoordinate); - emit coordinateChanged(_finalApproachCoordinate); _calcGlideSlope(); _ignoreRecalcSignals = false; } @@ -676,7 +676,6 @@ void LandingComplexItem::_updateFinalApproachCoodinateAltitudeFromFact(void) { _finalApproachCoordinate.setAltitude(finalApproachAltitude()->rawValue().toDouble()); emit finalApproachCoordinateChanged(_finalApproachCoordinate); - emit coordinateChanged(_finalApproachCoordinate); } void LandingComplexItem::_updateLandingCoodinateAltitudeFromFact(void) @@ -822,7 +821,9 @@ bool LandingComplexItem::_load(const QJsonObject& complexObject, int sequenceNum _ignoreRecalcSignals = false; _recalcFromCoordinateChange(); - emit coordinateChanged(this->coordinate()); // This will kick off terrain query + // These will kick off terrain query + emit finalApproachCoordinateChanged(_finalApproachCoordinate); + emit landingCoordinateChanged(_landingCoordinate); return true; } diff --git a/src/MissionManager/LandingComplexItem.h b/src/MissionManager/LandingComplexItem.h index edebb2fad4dd..6643561c999e 100644 --- a/src/MissionManager/LandingComplexItem.h +++ b/src/MissionManager/LandingComplexItem.h @@ -89,7 +89,8 @@ class LandingComplexItem : public ComplexMissionItem QString commandDescription (void) const final { return "Landing Pattern"; } QString commandName (void) const final { return "Landing Pattern"; } QString abbreviation (void) const final { return "L"; } - QGeoCoordinate coordinate (void) const final { return _finalApproachCoordinate; } + QGeoCoordinate coordinate (void) const final { return exitCoordinate(); } + QGeoCoordinate entryCoordinate (void) const final { return _finalApproachCoordinate; } QGeoCoordinate exitCoordinate (void) const final { return _landingCoordinate; } int sequenceNumber (void) const final { return _sequenceNumber; } double specifiedFlightSpeed (void) final { return std::numeric_limits::quiet_NaN(); } diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 61a506fcfa60..459fa023a3f3 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -1113,7 +1113,7 @@ void MissionController::save(QJsonObject& json) void MissionController::_calcPrevWaypointValues(VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference) { - QGeoCoordinate currentCoord = currentItem->coordinate(); + QGeoCoordinate currentCoord = currentItem->entryCoordinate(); QGeoCoordinate prevCoord = prevItem->exitCoordinate(); // Convert to fixed altitudes @@ -1125,7 +1125,7 @@ void MissionController::_calcPrevWaypointValues(VisualMissionItem* currentItem, double MissionController::_calcDistanceToHome(VisualMissionItem* currentItem, VisualMissionItem* homeItem) { - QGeoCoordinate currentCoord = currentItem->coordinate(); + QGeoCoordinate currentCoord = currentItem->entryCoordinate(); QGeoCoordinate homeCoord = homeItem->exitCoordinate(); bool distanceOk = false; @@ -1141,7 +1141,7 @@ FlightPathSegment* MissionController::_createFlightPathSegmentWorker(VisualItemP bool takeoffStraightUp = pair.second->isTakeoffItem() && !_controllerVehicle->fixedWing(); QGeoCoordinate coord1 = pair.first->exitCoordinate(); - QGeoCoordinate coord2 = pair.second->coordinate(); + QGeoCoordinate coord2 = pair.second->entryCoordinate(); double coord2AMSLAlt = pair.second->amslEntryAlt(); double coord1AMSLAlt = takeoffStraightUp ? coord2AMSLAlt : pair.first->amslExitAlt(); @@ -1159,11 +1159,12 @@ FlightPathSegment* MissionController::_createFlightPathSegmentWorker(VisualItemP } else { connect(pair.first, &VisualMissionItem::amslExitAltChanged, segment, &FlightPathSegment::setCoord1AMSLAlt); } - connect(pair.first, &VisualMissionItem::exitCoordinateChanged, segment, &FlightPathSegment::setCoordinate1); - connect(pair.second, &VisualMissionItem::coordinateChanged, segment, &FlightPathSegment::setCoordinate2); - connect(pair.second, &VisualMissionItem::amslEntryAltChanged, segment, &FlightPathSegment::setCoord2AMSLAlt); + connect(pair.first, &VisualMissionItem::exitCoordinateChanged, segment, &FlightPathSegment::setCoordinate1); + connect(pair.second, &VisualMissionItem::entryCoordinateChanged, segment, &FlightPathSegment::setCoordinate2); + connect(pair.second, &VisualMissionItem::amslEntryAltChanged, segment, &FlightPathSegment::setCoord2AMSLAlt); - connect(pair.second, &VisualMissionItem::coordinateChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection); + connect(pair.second, &VisualMissionItem::entryCoordinateChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection); + connect(pair.second, &VisualMissionItem::exitCoordinateChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection); connect(segment, &FlightPathSegment::totalDistanceChanged, this, &MissionController::recalcTerrainProfile, Qt::QueuedConnection); connect(segment, &FlightPathSegment::coord1AMSLAltChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection); @@ -1385,9 +1386,9 @@ void MissionController::_recalcFlightPathSegments(void) // Create a new segment. Since this is the fly view there is no need to wire change signals or worry about correct SegmentType coordVector = new FlightPathSegment( FlightPathSegment::SegmentTypeGeneric, - lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->coordinate() : lastSegmentVisualItemPair.first->exitCoordinate(), + lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->entryCoordinate() : lastSegmentVisualItemPair.first->exitCoordinate(), lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->amslEntryAlt() : lastSegmentVisualItemPair.first->amslExitAlt(), - lastSegmentVisualItemPair.second->coordinate(), + lastSegmentVisualItemPair.second->entryCoordinate(), lastSegmentVisualItemPair.second->amslEntryAlt(), !_flyView /* queryTerrainData */, this); @@ -1593,7 +1594,7 @@ void MissionController::_recalcMissionFlightStatus() if (qIsNaN(newVehicleYaw)) { // No specific vehicle yaw set. Current vehicle yaw is determined from flight path segment direction. if (simpleItem != lastFlyThroughVI) { - _missionFlightStatus.vehicleYaw = lastFlyThroughVI->exitCoordinate().azimuthTo(simpleItem->coordinate()); + _missionFlightStatus.vehicleYaw = lastFlyThroughVI->exitCoordinate().azimuthTo(simpleItem->entryCoordinate()); } } else { _missionFlightStatus.vehicleYaw = newVehicleYaw; @@ -1954,9 +1955,9 @@ void MissionController::_setPlannedHomePositionFromFirstCoordinate(const QGeoCoo for (int i=1; i<_visualItems->count(); i++) { VisualMissionItem* item = _visualItems->value(i); - if (item->specifiesCoordinate() && item->coordinate().isValid()) { + if (item->specifiesCoordinate() && item->entryCoordinate().isValid()) { foundFirstCoordinate = true; - firstCoordinate = item->coordinate(); + firstCoordinate = item->entryCoordinate(); break; } } @@ -2244,17 +2245,17 @@ void MissionController::_centerHomePositionOnMissionItems(QmlObjectListModel *vi VisualMissionItem* item = qobject_cast(visualItems->get(i)); if (item->specifiesCoordinate()) { if (firstCoordSet) { - double lat = _normalizeLat(item->coordinate().latitude()); - double lon = _normalizeLon(item->coordinate().longitude()); + double lat = _normalizeLat(item->entryCoordinate().latitude()); + double lon = _normalizeLon(item->entryCoordinate().longitude()); north = fmax(north, lat); south = fmin(south, lat); east = fmax(east, lon); west = fmin(west, lon); } else { firstCoordSet = true; - north = _normalizeLat(item->coordinate().latitude()); + north = _normalizeLat(item->entryCoordinate().latitude()); south = north; - east = _normalizeLon(item->coordinate().longitude()); + east = _normalizeLon(item->entryCoordinate().longitude()); west = east; } } @@ -2728,8 +2729,8 @@ void MissionController::_updateTimeout() if(pComplexItem) { QGCGeoBoundingCube bc = pComplexItem->boundingCube(); if(bc.isValid()) { - if(!firstCoordinate.isValid() && pComplexItem->coordinate().isValid()) { - firstCoordinate = pComplexItem->coordinate(); + if(!firstCoordinate.isValid() && pComplexItem->entryCoordinate().isValid()) { + firstCoordinate = pComplexItem->entryCoordinate(); } north = fmax(north, bc.pointNW.latitude() + 90.0); south = fmin(south, bc.pointSE.latitude() + 90.0); diff --git a/src/MissionManager/MissionSettingsItem.cc b/src/MissionManager/MissionSettingsItem.cc index ec1f92728593..7aa007a40ad9 100644 --- a/src/MissionManager/MissionSettingsItem.cc +++ b/src/MissionManager/MissionSettingsItem.cc @@ -41,6 +41,8 @@ MissionSettingsItem::MissionSettingsItem(PlanMasterController* masterController, connect(&_cameraSection, &CameraSection::specifiedGimbalYawChanged, this, &MissionSettingsItem::specifiedGimbalYawChanged); connect(&_cameraSection, &CameraSection::specifiedGimbalPitchChanged, this, &MissionSettingsItem::specifiedGimbalPitchChanged); connect(&_speedSection, &SpeedSection::specifiedFlightSpeedChanged, this, &MissionSettingsItem::specifiedFlightSpeedChanged); + connect(this, &MissionSettingsItem::coordinateChanged, this, &MissionSettingsItem::entryCoordinateChanged); + connect(this, &MissionSettingsItem::coordinateChanged, this, &MissionSettingsItem::exitCoordinateChanged); connect(this, &MissionSettingsItem::coordinateChanged, this, &MissionSettingsItem::_amslEntryAltChanged); connect(this, &MissionSettingsItem::amslEntryAltChanged, this, &MissionSettingsItem::amslExitAltChanged); connect(this, &MissionSettingsItem::amslEntryAltChanged, this, &MissionSettingsItem::minAMSLAltitudeChanged); @@ -176,7 +178,6 @@ void MissionSettingsItem::_setCoordinateWorker(const QGeoCoordinate& coordinate) if (_plannedHomePositionCoordinate != coordinate) { _plannedHomePositionCoordinate = coordinate; emit coordinateChanged(coordinate); - emit exitCoordinateChanged(coordinate); if (_plannedHomePositionFromVehicle) { _plannedHomePositionAltitudeFact.setRawValue(coordinate.altitude()); } @@ -252,7 +253,6 @@ void MissionSettingsItem::_updateAltitudeInCoordinate(QVariant value) qCDebug(MissionSettingsItemLog) << "MissionSettingsItem::_updateAltitudeInCoordinate" << newAltitude; _plannedHomePositionCoordinate.setAltitude(newAltitude); emit coordinateChanged(_plannedHomePositionCoordinate); - emit exitCoordinateChanged(_plannedHomePositionCoordinate); } } diff --git a/src/MissionManager/MissionSettingsItem.h b/src/MissionManager/MissionSettingsItem.h index 61c71ab6e298..21945bf5c575 100644 --- a/src/MissionManager/MissionSettingsItem.h +++ b/src/MissionManager/MissionSettingsItem.h @@ -67,7 +67,8 @@ class MissionSettingsItem : public ComplexMissionItem QString commandName (void) const final { return tr("Initial Camera Settings"); } QString abbreviation (void) const final; QGeoCoordinate coordinate (void) const final { return _plannedHomePositionCoordinate; } // Includes altitude - QGeoCoordinate exitCoordinate (void) const final { return _plannedHomePositionCoordinate; } + QGeoCoordinate entryCoordinate (void) const final { return coordinate(); } + QGeoCoordinate exitCoordinate (void) const final { return coordinate(); } int sequenceNumber (void) const final { return _sequenceNumber; } double specifiedGimbalYaw (void) final; double specifiedGimbalPitch (void) final; diff --git a/src/MissionManager/SimpleMissionItem.cc b/src/MissionManager/SimpleMissionItem.cc index 4b50e43891eb..88934728c049 100644 --- a/src/MissionManager/SimpleMissionItem.cc +++ b/src/MissionManager/SimpleMissionItem.cc @@ -147,6 +147,7 @@ void SimpleMissionItem::_connectSignals(void) connect(&_missionItem._param3Fact, &Fact::valueChanged, this, &SimpleMissionItem::_possibleRadiusChanged); // Exit coordinate is the same as entrance coordinate + connect(this, &SimpleMissionItem::coordinateChanged, this, &SimpleMissionItem::entryCoordinateChanged); connect(this, &SimpleMissionItem::coordinateChanged, this, &SimpleMissionItem::exitCoordinateChanged); // The following changes may also change friendlyEditAllowed diff --git a/src/MissionManager/SimpleMissionItem.h b/src/MissionManager/SimpleMissionItem.h index 6a071d9db836..4660d2df223b 100644 --- a/src/MissionManager/SimpleMissionItem.h +++ b/src/MissionManager/SimpleMissionItem.h @@ -111,6 +111,7 @@ class SimpleMissionItem : public VisualMissionItem QString commandName (void) const final; QString abbreviation (void) const final; QGeoCoordinate coordinate (void) const final; + QGeoCoordinate entryCoordinate (void) const final { return coordinate(); } QGeoCoordinate exitCoordinate (void) const final { return coordinate(); } double amslEntryAlt (void) const final; double amslExitAlt (void) const final { return amslEntryAlt(); } diff --git a/src/MissionManager/StructureScanComplexItem.cc b/src/MissionManager/StructureScanComplexItem.cc index 6fc2ba29467f..ae6ad3dec442 100644 --- a/src/MissionManager/StructureScanComplexItem.cc +++ b/src/MissionManager/StructureScanComplexItem.cc @@ -74,6 +74,9 @@ StructureScanComplexItem::StructureScanComplexItem(PlanMasterController* masterC connect(this, &StructureScanComplexItem::wizardModeChanged, this, &StructureScanComplexItem::readyForSaveStateChanged); + connect(this, &StructureScanComplexItem::coordinateChanged, this, &StructureScanComplexItem::entryCoordinateChanged); + connect(this, &StructureScanComplexItem::coordinateChanged, this, &StructureScanComplexItem::exitCoordinateChanged); + connect(&_entranceAltFact, &Fact::valueChanged, this, &StructureScanComplexItem::_amslEntryAltChanged); connect(this, &StructureScanComplexItem::amslEntryAltChanged, this, &StructureScanComplexItem::amslExitAltChanged); @@ -270,7 +273,6 @@ void StructureScanComplexItem::_flightPathChanged(void) QGeoCoordinate(south - 90.0, east - 180.0, top))); emit coordinateChanged(coordinate()); - emit exitCoordinateChanged(exitCoordinate()); emit greatestDistanceToChanged(); if (_isIncomplete) { @@ -491,7 +493,6 @@ QGeoCoordinate StructureScanComplexItem::coordinate(void) const void StructureScanComplexItem::_updateCoordinateAltitudes(void) { emit coordinateChanged(coordinate()); - emit exitCoordinateChanged(exitCoordinate()); } void StructureScanComplexItem::rotateEntryPoint(void) @@ -501,7 +502,6 @@ void StructureScanComplexItem::rotateEntryPoint(void) _entryVertex = 0; } emit coordinateChanged(coordinate()); - emit exitCoordinateChanged(exitCoordinate()); } void StructureScanComplexItem::_rebuildFlightPolygon(void) @@ -521,7 +521,6 @@ void StructureScanComplexItem::_rebuildFlightPolygon(void) } emit coordinateChanged(coordinate()); - emit exitCoordinateChanged(exitCoordinate()); } void StructureScanComplexItem::_recalcCameraShots(void) diff --git a/src/MissionManager/StructureScanComplexItem.h b/src/MissionManager/StructureScanComplexItem.h index e7b4d6d85987..615fff4380a4 100644 --- a/src/MissionManager/StructureScanComplexItem.h +++ b/src/MissionManager/StructureScanComplexItem.h @@ -70,6 +70,7 @@ class StructureScanComplexItem : public ComplexMissionItem QString commandName (void) const final { return tr("Structure Scan"); } QString abbreviation (void) const final { return "S"; } QGeoCoordinate coordinate (void) const final; + QGeoCoordinate entryCoordinate (void) const final { return coordinate(); } QGeoCoordinate exitCoordinate (void) const final { return coordinate(); } int sequenceNumber (void) const final { return _sequenceNumber; } double specifiedFlightSpeed (void) final { return std::numeric_limits::quiet_NaN(); } diff --git a/src/MissionManager/TransectStyleComplexItem.cc b/src/MissionManager/TransectStyleComplexItem.cc index 88a8754fca30..2f3508fecdc7 100644 --- a/src/MissionManager/TransectStyleComplexItem.cc +++ b/src/MissionManager/TransectStyleComplexItem.cc @@ -87,6 +87,9 @@ TransectStyleComplexItem::TransectStyleComplexItem(PlanMasterController* masterC connect(&_hoverAndCaptureFact, &Fact::rawValueChanged, this, &TransectStyleComplexItem::_handleHoverAndCaptureEnabled); + // The main coordinate is aliased to the entry + connect(this, &TransectStyleComplexItem::entryCoordinateChanged, this, &TransectStyleComplexItem::coordinateChanged); + connect(this, &TransectStyleComplexItem::visualTransectPointsChanged, this, &TransectStyleComplexItem::complexDistanceChanged); connect(this, &TransectStyleComplexItem::visualTransectPointsChanged, this, &TransectStyleComplexItem::greatestDistanceToChanged); connect(this, &TransectStyleComplexItem::wizardModeChanged, this, &TransectStyleComplexItem::readyForSaveStateChanged); @@ -227,7 +230,7 @@ bool TransectStyleComplexItem::_load(const QJsonObject& complexObject, bool forP if (!JsonHelper::loadGeoCoordinateArray(innerObject[_jsonVisualTransectPointsKey], false /* altitudeRequired */, _visualTransectPoints, errorString)) { return false; } - _coordinate = _visualTransectPoints.count() ? _visualTransectPoints.first().value() : QGeoCoordinate(); + _entryCoordinate = _visualTransectPoints.count() ? _visualTransectPoints.first().value() : QGeoCoordinate(); _exitCoordinate = _visualTransectPoints.count() ? _visualTransectPoints.last().value() : QGeoCoordinate(); _isIncomplete = false; @@ -349,7 +352,7 @@ void TransectStyleComplexItem::_setIfDirty(bool dirty) void TransectStyleComplexItem::_updateCoordinateAltitudes(void) { - emit coordinateChanged(coordinate()); + emit entryCoordinateChanged(entryCoordinate()); emit exitCoordinateChanged(exitCoordinate()); } @@ -434,10 +437,9 @@ void TransectStyleComplexItem::_rebuildTransects(void) QGeoCoordinate(south - 90.0, east - 180.0, top))); emit visualTransectPointsChanged(); - _coordinate = _visualTransectPoints.count() ? _visualTransectPoints.first().value() : QGeoCoordinate(); + _entryCoordinate = _visualTransectPoints.count() ? _visualTransectPoints.first().value() : QGeoCoordinate(); _exitCoordinate = _visualTransectPoints.count() ? _visualTransectPoints.last().value() : QGeoCoordinate(); - emit coordinateChanged(_coordinate); - emit exitCoordinateChanged(_exitCoordinate); + _updateCoordinateAltitudes(); if (_isIncomplete) { _isIncomplete = false; diff --git a/src/MissionManager/TransectStyleComplexItem.h b/src/MissionManager/TransectStyleComplexItem.h index edc1c70e76ec..f92def1bc5d1 100644 --- a/src/MissionManager/TransectStyleComplexItem.h +++ b/src/MissionManager/TransectStyleComplexItem.h @@ -81,7 +81,8 @@ class TransectStyleComplexItem : public ComplexMissionItem bool isSimpleItem (void) const final { return false; } bool isStandaloneCoordinate (void) const final { return false; } bool specifiesAltitudeOnly (void) const final { return false; } - QGeoCoordinate coordinate (void) const final { return _coordinate; } + QGeoCoordinate coordinate (void) const final { return entryCoordinate(); } + QGeoCoordinate entryCoordinate (void) const final { return _entryCoordinate; } QGeoCoordinate exitCoordinate (void) const final { return _exitCoordinate; } int sequenceNumber (void) const final { return _sequenceNumber; } double specifiedFlightSpeed (void) final { return std::numeric_limits::quiet_NaN(); } @@ -146,7 +147,7 @@ protected slots: void _recalcComplexDistance (void); int _sequenceNumber = 0; - QGeoCoordinate _coordinate; + QGeoCoordinate _entryCoordinate; QGeoCoordinate _exitCoordinate; QGCMapPolygon _surveyAreaPolygon; diff --git a/src/MissionManager/VisualMissionItem.h b/src/MissionManager/VisualMissionItem.h index 44d3b9727e27..8b639c65358c 100644 --- a/src/MissionManager/VisualMissionItem.h +++ b/src/MissionManager/VisualMissionItem.h @@ -42,9 +42,10 @@ class VisualMissionItem : public QObject Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged) ///< Does not include altitude Q_PROPERTY(double amslEntryAlt READ amslEntryAlt NOTIFY amslEntryAltChanged) Q_PROPERTY(double terrainAltitude READ terrainAltitude NOTIFY terrainAltitudeChanged) ///< The altitude of terrain at the coordinate position, NaN if not known + Q_PROPERTY(QGeoCoordinate entryCoordinate READ entryCoordinate NOTIFY entryCoordinateChanged) ///< Does not include altitude Q_PROPERTY(QGeoCoordinate exitCoordinate READ exitCoordinate NOTIFY exitCoordinateChanged) ///< Does not include altitude Q_PROPERTY(double amslExitAlt READ amslExitAlt NOTIFY amslExitAltChanged) - Q_PROPERTY(bool exitCoordinateSameAsEntry READ exitCoordinateSameAsEntry NOTIFY exitCoordinateSameAsEntryChanged) ///< true: exitCoordinate and coordinate are the same value + Q_PROPERTY(bool exitCoordinateSameAsEntry READ exitCoordinateSameAsEntry NOTIFY exitCoordinateSameAsEntryChanged) ///< true: entryCoordinate and exitCoordinate are the same value Q_PROPERTY(QString commandDescription READ commandDescription NOTIFY commandDescriptionChanged) Q_PROPERTY(QString commandName READ commandName NOTIFY commandNameChanged) Q_PROPERTY(QString abbreviation READ abbreviation NOTIFY abbreviationChanged) @@ -140,6 +141,7 @@ class VisualMissionItem : public QObject virtual QString commandName (void) const = 0; virtual QString abbreviation (void) const = 0; virtual QGeoCoordinate coordinate (void) const = 0; + virtual QGeoCoordinate entryCoordinate (void) const = 0; virtual QGeoCoordinate exitCoordinate (void) const = 0; virtual double amslEntryAlt (void) const = 0; virtual double amslExitAlt (void) const = 0; @@ -202,6 +204,7 @@ class VisualMissionItem : public QObject void commandNameChanged (void); void abbreviationChanged (void); void coordinateChanged (const QGeoCoordinate& coordinate); + void entryCoordinateChanged (const QGeoCoordinate& entryCoordinate); void exitCoordinateChanged (const QGeoCoordinate& exitCoordinate); void dirtyChanged (bool dirty); void distanceChanged (double distance); diff --git a/test/MissionManager/SimpleMissionItemTest.cc b/test/MissionManager/SimpleMissionItemTest.cc index 1644a3ad7d95..276520e569f7 100644 --- a/test/MissionManager/SimpleMissionItemTest.cc +++ b/test/MissionManager/SimpleMissionItemTest.cc @@ -203,14 +203,14 @@ void SimpleMissionItemTest::_testSignals() // Check that actually changing coordinate signals correctly _simpleItem->setCoordinate(QGeoCoordinate(missionItem.param5() + 1, missionItem.param6(), missionItem.param7())); QVERIFY(_spyVisualItem->onlyEmittedOnceByMask( - _spyVisualItem->mask("coordinateChanged", "exitCoordinateChanged", "dirtyChanged", "amslEntryAltChanged", - "amslExitAltChanged", "terrainAltitudeChanged"))); + _spyVisualItem->mask("coordinateChanged", "entryCoordinateChanged", "exitCoordinateChanged", "dirtyChanged", + "amslEntryAltChanged", "amslExitAltChanged", "terrainAltitudeChanged"))); _spyVisualItem->clearAllSignals(); _spySimpleItem->clearAllSignals(); _simpleItem->setCoordinate(QGeoCoordinate(missionItem.param5(), missionItem.param6() + 1, missionItem.param7())); QVERIFY(_spyVisualItem->onlyEmittedOnceByMask( - _spyVisualItem->mask("coordinateChanged", "exitCoordinateChanged", "dirtyChanged", "amslEntryAltChanged", - "amslExitAltChanged", "terrainAltitudeChanged"))); + _spyVisualItem->mask("coordinateChanged", "entryCoordinateChanged", "exitCoordinateChanged", "dirtyChanged", + "amslEntryAltChanged", "amslExitAltChanged", "terrainAltitudeChanged"))); _spyVisualItem->clearAllSignals(); _spySimpleItem->clearAllSignals(); // Altitude in coordinate is not used in setCoordinate From 042cf64c65fee490759c71270136d651c92a2e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Thu, 8 Jan 2026 12:50:51 +0100 Subject: [PATCH 041/119] Add editableAlt getter to VisualMissionItem Introduced a new editableAlt getter on VisualMissionItem to expose a single source of truth for the primary user-editable altitude across all VisualMissionItem subclasses. This value is intentionally independent of altitude reference-frame (relative, absolute, terrain), as it is intended to be used in conjunction with the likewise frame-agnostic VisualMissionItem::applyNewAltitude method to provide an external interface for generic altitude offsetting. --- src/MissionManager/LandingComplexItem.cc | 5 +++++ src/MissionManager/LandingComplexItem.h | 1 + src/MissionManager/MissionSettingsItem.h | 1 + src/MissionManager/SimpleMissionItem.cc | 5 +++++ src/MissionManager/SimpleMissionItem.h | 1 + src/MissionManager/StructureScanComplexItem.cc | 5 +++++ src/MissionManager/StructureScanComplexItem.h | 1 + src/MissionManager/TransectStyleComplexItem.cc | 5 +++++ src/MissionManager/TransectStyleComplexItem.h | 1 + src/MissionManager/VisualMissionItem.h | 1 + 10 files changed, 26 insertions(+) diff --git a/src/MissionManager/LandingComplexItem.cc b/src/MissionManager/LandingComplexItem.cc index 0f6dff5034df..98aad80b41a9 100644 --- a/src/MissionManager/LandingComplexItem.cc +++ b/src/MissionManager/LandingComplexItem.cc @@ -656,6 +656,11 @@ void LandingComplexItem::setSequenceNumber(int sequenceNumber) } } +double LandingComplexItem::editableAlt() const +{ + return finalApproachAltitude()->rawValue().toDouble(); +} + double LandingComplexItem::amslEntryAlt(void) const { return finalApproachAltitude()->rawValue().toDouble() + (_altitudesAreRelative ? _missionController->plannedHomePosition().altitude() : 0); diff --git a/src/MissionManager/LandingComplexItem.h b/src/MissionManager/LandingComplexItem.h index 6643561c999e..602e7748fc56 100644 --- a/src/MissionManager/LandingComplexItem.h +++ b/src/MissionManager/LandingComplexItem.h @@ -104,6 +104,7 @@ class LandingComplexItem : public ComplexMissionItem void setDirty (bool dirty) final; void setCoordinate (const QGeoCoordinate& coordinate) final; void setSequenceNumber (int sequenceNumber) final; + double editableAlt (void) const final; double amslEntryAlt (void) const final; double amslExitAlt (void) const final; double minAMSLAltitude (void) const final { return amslExitAlt(); } diff --git a/src/MissionManager/MissionSettingsItem.h b/src/MissionManager/MissionSettingsItem.h index 21945bf5c575..d4fd19ba8da8 100644 --- a/src/MissionManager/MissionSettingsItem.h +++ b/src/MissionManager/MissionSettingsItem.h @@ -81,6 +81,7 @@ class MissionSettingsItem : public ComplexMissionItem void setCoordinate (const QGeoCoordinate& coordinate) final; // Should only be called if the end user is moving void setSequenceNumber (int sequenceNumber) final; void save (QJsonArray& missionItems) final; + double editableAlt (void) const final { return _plannedHomePositionAltitudeFact.rawValue().toDouble(); } double amslEntryAlt (void) const final { return _plannedHomePositionCoordinate.altitude(); } double amslExitAlt (void) const final { return amslEntryAlt(); } double minAMSLAltitude (void) const final { return amslEntryAlt(); } diff --git a/src/MissionManager/SimpleMissionItem.cc b/src/MissionManager/SimpleMissionItem.cc index 88934728c049..b79536ad8c14 100644 --- a/src/MissionManager/SimpleMissionItem.cc +++ b/src/MissionManager/SimpleMissionItem.cc @@ -1105,6 +1105,11 @@ QGeoCoordinate SimpleMissionItem::coordinate(void) const } } +double SimpleMissionItem::editableAlt() const +{ + return _missionItem.param7(); +} + double SimpleMissionItem::amslEntryAlt(void) const { switch (_altitudeMode) { diff --git a/src/MissionManager/SimpleMissionItem.h b/src/MissionManager/SimpleMissionItem.h index 4660d2df223b..fa859674d668 100644 --- a/src/MissionManager/SimpleMissionItem.h +++ b/src/MissionManager/SimpleMissionItem.h @@ -113,6 +113,7 @@ class SimpleMissionItem : public VisualMissionItem QGeoCoordinate coordinate (void) const final; QGeoCoordinate entryCoordinate (void) const final { return coordinate(); } QGeoCoordinate exitCoordinate (void) const final { return coordinate(); } + double editableAlt (void) const final; double amslEntryAlt (void) const final; double amslExitAlt (void) const final { return amslEntryAlt(); } int sequenceNumber (void) const final { return _missionItem.sequenceNumber(); } diff --git a/src/MissionManager/StructureScanComplexItem.cc b/src/MissionManager/StructureScanComplexItem.cc index ae6ad3dec442..52b6035aefb9 100644 --- a/src/MissionManager/StructureScanComplexItem.cc +++ b/src/MissionManager/StructureScanComplexItem.cc @@ -639,6 +639,11 @@ void StructureScanComplexItem::_updateWizardMode(void) } } +double StructureScanComplexItem::editableAlt() const +{ + return _entranceAltFact.rawValue().toDouble(); +} + double StructureScanComplexItem::amslEntryAlt(void) const { return _entranceAltFact.rawValue().toDouble() + _missionController->plannedHomePosition().altitude(); diff --git a/src/MissionManager/StructureScanComplexItem.h b/src/MissionManager/StructureScanComplexItem.h index 615fff4380a4..97d5115cac38 100644 --- a/src/MissionManager/StructureScanComplexItem.h +++ b/src/MissionManager/StructureScanComplexItem.h @@ -86,6 +86,7 @@ class StructureScanComplexItem : public ComplexMissionItem void setCoordinate (const QGeoCoordinate& coordinate) final { Q_UNUSED(coordinate); } void setSequenceNumber (int sequenceNumber) final; void save (QJsonArray& missionItems) final; + double editableAlt (void) const final; double amslEntryAlt (void) const final; double amslExitAlt (void) const final { return amslEntryAlt(); }; double minAMSLAltitude (void) const final; diff --git a/src/MissionManager/TransectStyleComplexItem.cc b/src/MissionManager/TransectStyleComplexItem.cc index 2f3508fecdc7..061609fa8540 100644 --- a/src/MissionManager/TransectStyleComplexItem.cc +++ b/src/MissionManager/TransectStyleComplexItem.cc @@ -1322,6 +1322,11 @@ void TransectStyleComplexItem::_recalcComplexDistance(void) emit complexDistanceChanged(); } +double TransectStyleComplexItem::editableAlt() const +{ + return _cameraCalc.distanceToSurface()->rawValue().toDouble(); +} + double TransectStyleComplexItem::amslEntryAlt(void) const { double alt = qQNaN(); diff --git a/src/MissionManager/TransectStyleComplexItem.h b/src/MissionManager/TransectStyleComplexItem.h index f92def1bc5d1..3855e9a5d7c6 100644 --- a/src/MissionManager/TransectStyleComplexItem.h +++ b/src/MissionManager/TransectStyleComplexItem.h @@ -97,6 +97,7 @@ class TransectStyleComplexItem : public ComplexMissionItem void setDirty (bool dirty) final; void setCoordinate (const QGeoCoordinate& coordinate) final { Q_UNUSED(coordinate); } void setSequenceNumber (int sequenceNumber) final; + double editableAlt (void) const final; double amslEntryAlt (void) const final; double amslExitAlt (void) const final; double minAMSLAltitude (void) const final; diff --git a/src/MissionManager/VisualMissionItem.h b/src/MissionManager/VisualMissionItem.h index 8b639c65358c..51df5fd8a26e 100644 --- a/src/MissionManager/VisualMissionItem.h +++ b/src/MissionManager/VisualMissionItem.h @@ -143,6 +143,7 @@ class VisualMissionItem : public QObject virtual QGeoCoordinate coordinate (void) const = 0; virtual QGeoCoordinate entryCoordinate (void) const = 0; virtual QGeoCoordinate exitCoordinate (void) const = 0; + virtual double editableAlt (void) const = 0; virtual double amslEntryAlt (void) const = 0; virtual double amslExitAlt (void) const = 0; virtual int sequenceNumber (void) const = 0; From fbe2d4f725fad53ed81f1653b888626c88ca36e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Mon, 2 Mar 2026 13:48:13 +0100 Subject: [PATCH 042/119] Implement setCoordinate for complex scan items --- src/MissionManager/CorridorScanComplexItem.cc | 19 ++++++++++++++++++ src/MissionManager/CorridorScanComplexItem.h | 1 + .../StructureScanComplexItem.cc | 20 +++++++++++++++++++ src/MissionManager/StructureScanComplexItem.h | 2 +- .../TransectStyleComplexItem.cc | 19 ++++++++++++++++++ src/MissionManager/TransectStyleComplexItem.h | 2 +- 6 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/MissionManager/CorridorScanComplexItem.cc b/src/MissionManager/CorridorScanComplexItem.cc index 8a64cb7e5555..7c13502a6bba 100644 --- a/src/MissionManager/CorridorScanComplexItem.cc +++ b/src/MissionManager/CorridorScanComplexItem.cc @@ -154,6 +154,25 @@ bool CorridorScanComplexItem::specifiesCoordinate(void) const return _corridorPolyline.count() > 1; } +void CorridorScanComplexItem::setCoordinate(const QGeoCoordinate& coordinate) +{ + if (!coordinate.isValid() || !_entryCoordinate.isValid() || _corridorPolyline.count() < 2) { + return; + } + + const double distanceMeters = _entryCoordinate.distanceTo(coordinate); + const double azimuthDegrees = _entryCoordinate.azimuthTo(coordinate); + const QList vertices = _corridorPolyline.coordinateList(); + + QList translatedVertices; + translatedVertices.reserve(vertices.count()); + for (const QGeoCoordinate& vertex: vertices) { + translatedVertices.append(vertex.atDistanceAndAzimuth(distanceMeters, azimuthDegrees)); + } + + _corridorPolyline.setPath(translatedVertices); +} + int CorridorScanComplexItem::_calcTransectCount(void) const { double fullWidth = _corridorWidthFact.rawValue().toDouble(); diff --git a/src/MissionManager/CorridorScanComplexItem.h b/src/MissionManager/CorridorScanComplexItem.h index 4030db4480fa..c061070ad56f 100644 --- a/src/MissionManager/CorridorScanComplexItem.h +++ b/src/MissionManager/CorridorScanComplexItem.h @@ -52,6 +52,7 @@ class CorridorScanComplexItem : public TransectStyleComplexItem QString commandDescription (void) const final { return tr("Corridor Scan"); } QString commandName (void) const final { return tr("Corridor Scan"); } QString abbreviation (void) const final { return tr("C"); } + void setCoordinate (const QGeoCoordinate& coordinate) final; ReadyForSaveState readyForSaveState (void) const final; double additionalTimeDelay (void) const final { return 0; } diff --git a/src/MissionManager/StructureScanComplexItem.cc b/src/MissionManager/StructureScanComplexItem.cc index 52b6035aefb9..dd1955bcd9e2 100644 --- a/src/MissionManager/StructureScanComplexItem.cc +++ b/src/MissionManager/StructureScanComplexItem.cc @@ -468,6 +468,26 @@ void StructureScanComplexItem::applyNewAltitude(double newAltitude) _entranceAltFact.setRawValue(newAltitude); } +void StructureScanComplexItem::setCoordinate(const QGeoCoordinate& coordinate) +{ + const QGeoCoordinate oldCoordinate = this->coordinate(); + if (!oldCoordinate.isValid() || !coordinate.isValid() || _structurePolygon.count() < 3) { + return; + } + + const double distanceMeters = oldCoordinate.distanceTo(coordinate); + const double azimuthDegrees = oldCoordinate.azimuthTo(coordinate); + const QList vertices = _structurePolygon.coordinateList(); + + QList translatedVertices; + translatedVertices.reserve(vertices.count()); + for (const QGeoCoordinate& vertex: vertices) { + translatedVertices.append(vertex.atDistanceAndAzimuth(distanceMeters, azimuthDegrees)); + } + + _structurePolygon.setPath(translatedVertices); +} + void StructureScanComplexItem::_polygonDirtyChanged(bool dirty) { if (dirty) { diff --git a/src/MissionManager/StructureScanComplexItem.h b/src/MissionManager/StructureScanComplexItem.h index 97d5115cac38..79c322279136 100644 --- a/src/MissionManager/StructureScanComplexItem.h +++ b/src/MissionManager/StructureScanComplexItem.h @@ -83,7 +83,7 @@ class StructureScanComplexItem : public ComplexMissionItem ReadyForSaveState readyForSaveState (void) const final; bool exitCoordinateSameAsEntry (void) const final { return true; } void setDirty (bool dirty) final; - void setCoordinate (const QGeoCoordinate& coordinate) final { Q_UNUSED(coordinate); } + void setCoordinate (const QGeoCoordinate& coordinate) final; void setSequenceNumber (int sequenceNumber) final; void save (QJsonArray& missionItems) final; double editableAlt (void) const final; diff --git a/src/MissionManager/TransectStyleComplexItem.cc b/src/MissionManager/TransectStyleComplexItem.cc index 061609fa8540..23615ed80ce4 100644 --- a/src/MissionManager/TransectStyleComplexItem.cc +++ b/src/MissionManager/TransectStyleComplexItem.cc @@ -120,6 +120,25 @@ void TransectStyleComplexItem::setDirty(bool dirty) } } +void TransectStyleComplexItem::setCoordinate(const QGeoCoordinate& coordinate) +{ + if (!coordinate.isValid() || !_entryCoordinate.isValid() || _surveyAreaPolygon.count() < 3) { + return; + } + + const double distanceMeters = _entryCoordinate.distanceTo(coordinate); + const double azimuthDegrees = _entryCoordinate.azimuthTo(coordinate); + const QList vertices = _surveyAreaPolygon.coordinateList(); + + QList translatedVertices; + translatedVertices.reserve(vertices.count()); + for (const QGeoCoordinate& vertex: vertices) { + translatedVertices.append(vertex.atDistanceAndAzimuth(distanceMeters, azimuthDegrees)); + } + + _surveyAreaPolygon.setPath(translatedVertices); +} + void TransectStyleComplexItem::_save(QJsonObject& complexObject) { QJsonObject innerObject; diff --git a/src/MissionManager/TransectStyleComplexItem.h b/src/MissionManager/TransectStyleComplexItem.h index 3855e9a5d7c6..b147f1e634e8 100644 --- a/src/MissionManager/TransectStyleComplexItem.h +++ b/src/MissionManager/TransectStyleComplexItem.h @@ -95,7 +95,7 @@ class TransectStyleComplexItem : public ComplexMissionItem QString abbreviation (void) const override { return tr("T"); } bool exitCoordinateSameAsEntry (void) const final { return false; } void setDirty (bool dirty) final; - void setCoordinate (const QGeoCoordinate& coordinate) final { Q_UNUSED(coordinate); } + void setCoordinate (const QGeoCoordinate& coordinate) override; void setSequenceNumber (int sequenceNumber) final; double editableAlt (void) const final; double amslEntryAlt (void) const final; From 5f3e5c37df75107ee0c88bc16f31e6c22b80fbe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Thu, 8 Jan 2026 15:51:48 +0100 Subject: [PATCH 043/119] Add mission item bulk displacement utilities Introduced new Q_INVOKABLE helpers on MissionController to transform entire missions, including: - Repositioning the mission around a new home coordinate - Offsetting the mission in ENU space - Rotating the mission around home Optional flags allow takeoff and landing items to be included or excluded. Repositioning includes them by default, while offset and rotation do not. All operations require a valid planned home position to succeed. --- src/MissionManager/MissionController.cc | 134 ++++++++++++++++++++++++ src/MissionManager/MissionController.h | 44 ++++++++ 2 files changed, 178 insertions(+) diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 459fa023a3f3..769fc0a3fd0d 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -26,6 +26,7 @@ #include #include +#include #define UPDATE_TIMEOUT 5000 ///< How often we check for bounding box changes @@ -2678,6 +2679,139 @@ void MissionController::setCurrentPlanViewSeqNum(int sequenceNumber, bool force) } } +void MissionController::repositionMission(const QGeoCoordinate& newHome, + bool repositionTakeoffItems, + bool repositionLandingItems) +{ + if (!newHome.isValid()) { + qCWarning(MissionControllerLog) << "Cannot reposition mission to an invalid coordinate"; + return; + } + + if (!_settingsItem || !_settingsItem->coordinate().isValid()) { + qCWarning(MissionControllerLog) << "Cannot reposition mission while home is invalid"; + return; + } + + const QGeoCoordinate oldHome = _settingsItem->coordinate(); + + for (int i = 0; i < _visualItems->count(); ++i) { + auto* item = qobject_cast(_visualItems->get(i)); + if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) { + continue; + } + + if ((!repositionTakeoffItems && item->isTakeoffItem()) || + (!repositionLandingItems && item->isLandCommand())) { + continue; + } + + const QGeoCoordinate oldCoord = item->coordinate(); + if (!oldCoord.isValid()) { + continue; + } + + const double distanceMeters = oldHome.distanceTo(oldCoord); + const double azimuthDegrees = oldHome.azimuthTo(oldCoord); + + QGeoCoordinate newCoord = newHome.atDistanceAndAzimuth(distanceMeters, azimuthDegrees); + item->setCoordinate(newCoord); + } + + setDirty(true); +} + +void MissionController::offsetMission(double eastMeters, + double northMeters, + double upMeters, + bool offsetTakeoffItems, + bool offsetLandingItems) +{ + if (!_settingsItem || !_settingsItem->coordinate().isValid()) { + qCWarning(MissionControllerLog) << "Cannot offset mission while home is invalid"; + return; + } + + if (!qFuzzyIsNull(eastMeters) || !qFuzzyIsNull(northMeters)) { + double distanceMeters = qSqrt(eastMeters * eastMeters + northMeters * northMeters); + double azimuthDegrees = 0.0; + if (!qFuzzyIsNull(distanceMeters)) { + const double azimuthRadians = qAtan2(eastMeters, northMeters); + azimuthDegrees = qRadiansToDegrees(azimuthRadians); + } + + const QGeoCoordinate oldHome = _settingsItem->coordinate(); + QGeoCoordinate newHome = oldHome.atDistanceAndAzimuth(distanceMeters, azimuthDegrees); + repositionMission(newHome, offsetTakeoffItems, offsetLandingItems); + } + + if (!qFuzzyIsNull(upMeters)) { + for (int i = 0; i < _visualItems->count(); ++i) { + auto* item = qobject_cast(_visualItems->get(i)); + if (!item || item == _settingsItem) { + continue; + } + + if ((!offsetTakeoffItems && item->isTakeoffItem()) || + (!offsetLandingItems && item->isLandCommand())) { + continue; + } + + if (!item->specifiesCoordinate() && !item->specifiesAltitudeOnly()) { + continue; + } + + if (!qIsNaN(item->editableAlt())) { + item->applyNewAltitude(item->editableAlt() + upMeters); + } + } + } +} + +void MissionController::rotateMission(double degreesCW, + bool rotateTakeoffItems, + bool rotateLandingItems) +{ + if (qFuzzyIsNull(degreesCW)) { + return; + } + + if (!_settingsItem || !_settingsItem->coordinate().isValid()) { + qCWarning(MissionControllerLog) << "Cannot rotate mission while home is invalid"; + return; + } + + const QGeoCoordinate home = _settingsItem->coordinate(); + + for (int i = 0; i < _visualItems->count(); ++i) { + auto* item = qobject_cast(_visualItems->get(i)); + if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) { + continue; + } + + if ((!rotateTakeoffItems && item->isTakeoffItem()) || + (!rotateLandingItems && item->isLandCommand())) { + continue; + } + + const QGeoCoordinate oldCoord = item->coordinate(); + if (!oldCoord.isValid()) { + continue; + } + + const double distanceMeters = home.distanceTo(oldCoord); + if (qFuzzyIsNull(distanceMeters)) { + continue; + } + const double azimuthDegrees = home.azimuthTo(oldCoord); + + QGeoCoordinate newCoord = home.atDistanceAndAzimuth(distanceMeters, azimuthDegrees + degreesCW); + item->setCoordinate(newCoord); + } + + setDirty(true); +} + void MissionController::_updateTimeout() { QGeoCoordinate firstCoordinate; diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 3b7898d9f779..f0d2cd132da1 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "PlanElementController.h" @@ -182,6 +183,49 @@ class MissionController : public PlanElementController /// @param force - true: reset internals even if specified item is already selected Q_INVOKABLE void setCurrentPlanViewSeqNum(int sequenceNumber, bool force); + /// Repositions all mission items which specify a coordinate around a new + /// home coordinate. Requires a valid planned home position; otherwise the + /// mission is not modified and a warning is logged. + /// @param newHome New coordinate for the home item + /// @param repositionTakeoffItems If true, items identified as takeoff items + /// (isTakeoffItem) will be repositioned + /// @param repositionLandingItems If true, items identified as landing items + /// (isLandCommand) will be repositioned + Q_INVOKABLE void repositionMission(const QGeoCoordinate& newHome, + bool repositionTakeoffItems = true, + bool repositionLandingItems = true); + + /// Offsets all mission items which specify a coordinate by the specified + /// ENU amounts in meters. Home altitude remains unchanged. Requires a valid + /// planned home position; otherwise the mission is not modified and a + /// warning is logged. + /// @param eastMeters Distance to offset items to the east, in meters + /// @param northMeters Distance to offset items to the north, in meters + /// @param upMeters Distance to offset items upwards, in meters + /// @param offsetTakeoffItems If true, items identified as takeoff items + /// (isTakeoffItem) will be offset + /// @param offsetLandingItems If true, items identified as landing items + /// (isLandCommand) will be offset + Q_INVOKABLE void offsetMission(double eastMeters, + double northMeters, + double upMeters = 0.0, + bool offsetTakeoffItems = false, + bool offsetLandingItems = false); + + /// Rotates all mission items which specify a coordinate around the up axis + /// of the home position. Complex items are rotated by moving their + /// reference coordinate: their geometry and orientation are not modified. + /// Requires a valid planned home position; otherwise the mission is not + /// modified and a warning is logged. + /// @param degreesCW Angle to rotate items by, in degrees clockwise + /// @param rotateTakeoffItems If true, items identified as takeoff items + /// (isTakeoffItem) will be rotated + /// @param rotateLandingItems If true, items identified as landing items + /// (isLandCommand) will be rotated + Q_INVOKABLE void rotateMission(double degreesCW, + bool rotateTakeoffItems = false, + bool rotateLandingItems = false); + enum SendToVehiclePreCheckState { SendToVehiclePreCheckStateOk, // Ok to send plan to vehicle SendToVehiclePreCheckStateNoActiveVehicle, // There is no active vehicle From 78466baa898249f2503f33140864c3f842a8a720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Thu, 8 Jan 2026 16:00:59 +0100 Subject: [PATCH 044/119] Add hamburger menu with mission-wide transforms Added a hamburger menu to the Mission Settings item that provides options to offset, rotate, and reposition the entire mission. Also ensured DropPanel instances destroy themselves when closed to avoid leaking dynamically-created menus. --- src/QmlControls/MissionItemEditor.qml | 278 +++++++++++++++++++++++++- 1 file changed, 276 insertions(+), 2 deletions(-) diff --git a/src/QmlControls/MissionItemEditor.qml b/src/QmlControls/MissionItemEditor.qml index f760e9f0cbbc..8c08c08536e2 100644 --- a/src/QmlControls/MissionItemEditor.qml +++ b/src/QmlControls/MissionItemEditor.qml @@ -194,6 +194,7 @@ Rectangle { DropPanel { id: hamburgerMenuDropPanel + onClosed: destroy() sourceComponent: Component { ColumnLayout { @@ -271,6 +272,275 @@ Rectangle { } } + Component { + id: missionStartHamburgerMenuDropPanelComponent + + DropPanel { + id: missionStartHamburgerMenuDropPanel + onClosed: destroy() + + sourceComponent: Component { + ColumnLayout { + spacing: ScreenTools.defaultFontPixelHeight / 2 + + QGCButton { + Layout.fillWidth: true + text: qsTr("Offset mission...") + enabled: _missionController.plannedHomePosition.isValid + onClicked: { + offsetMissionDialogComponent.createObject(mainWindow).open() + missionStartHamburgerMenuDropPanel.close() + } + } + + QGCButton { + Layout.fillWidth: true + text: qsTr("Rotate mission...") + enabled: _missionController.plannedHomePosition.isValid + onClicked: { + rotateMissionDialogComponent.createObject(mainWindow).open() + missionStartHamburgerMenuDropPanel.close() + } + } + + QGCButton { + Layout.fillWidth: true + text: qsTr("Edit mission position...") + enabled: _missionController.plannedHomePosition.isValid + onClicked: { + editMissionPositionDialogComponent.createObject( + mainWindow, + { coordinate: _missionController.plannedHomePosition }).open() + missionStartHamburgerMenuDropPanel.close() + } + } + } + } + } + } + + Component { + id: offsetMissionDialogComponent + + QGCPopupDialog { + id: root + title: qsTr("Offset Mission") + buttons: Dialog.Cancel | Dialog.Ok + modal: true + + property real _margin: ScreenTools.defaultFontPixelWidth / 2 + property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 20 + property real _labelWidth: ScreenTools.defaultFontPixelWidth * 14 + + ColumnLayout { + spacing: _margin + + RowLayout { + spacing: _margin + Layout.fillWidth: true + + QGCLabel { + text: qsTr("East (m)") + Layout.preferredWidth: _labelWidth + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + QGCTextField { + id: eastField + text: "0" + Layout.preferredWidth: _textFieldWidth + Layout.fillWidth: true + inputMethodHints: Qt.ImhPreferNumbers + validator: DoubleValidator { + notation: DoubleValidator.StandardNotation + locale: Qt.locale() + } + } + } + + RowLayout { + spacing: _margin + Layout.fillWidth: true + + QGCLabel { + text: qsTr("North (m)") + Layout.preferredWidth: _labelWidth + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + QGCTextField { + id: northField + text: "0" + Layout.preferredWidth: _textFieldWidth + Layout.fillWidth: true + inputMethodHints: Qt.ImhPreferNumbers + validator: DoubleValidator { + notation: DoubleValidator.StandardNotation + locale: Qt.locale() + } + } + } + + RowLayout { + spacing: _margin + Layout.fillWidth: true + + QGCLabel { + text: qsTr("Up (m)") + Layout.preferredWidth: _labelWidth + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + QGCTextField { + id: upField + text: "0" + Layout.preferredWidth: _textFieldWidth + Layout.fillWidth: true + inputMethodHints: Qt.ImhPreferNumbers + validator: DoubleValidator { + notation: DoubleValidator.StandardNotation + locale: Qt.locale() + } + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + QGCCheckBox { + id: offsetTakeoffCheck + text: qsTr("Also move takeoff items") + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + } + + QGCCheckBox { + id: offsetLandingCheck + text: qsTr("Also move landing items") + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + } + } + + QGCLabel { + Layout.fillWidth: true + Layout.maximumWidth: _labelWidth + _textFieldWidth + + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + + text: qsTr("Note: Home altitude is not modified.") + } + } + + onAccepted: { + var east = Number.fromLocaleString(Qt.locale(), eastField.text) + var north = Number.fromLocaleString(Qt.locale(), northField.text) + var up = Number.fromLocaleString(Qt.locale(), upField.text) + + if (isNaN(east)) east = 0 + if (isNaN(north)) north = 0 + if (isNaN(up)) up = 0 + + _missionController.offsetMission( + east, + north, + up, + offsetTakeoffCheck.checked, + offsetLandingCheck.checked + ) + } + } + } + + Component { + id: rotateMissionDialogComponent + + QGCPopupDialog { + id: root + title: qsTr("Rotate Mission") + buttons: Dialog.Cancel | Dialog.Ok + modal: true + + property real _margin: ScreenTools.defaultFontPixelWidth / 2 + property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 20 + property real _labelWidth: ScreenTools.defaultFontPixelWidth * 14 + + ColumnLayout { + spacing: _margin + + RowLayout { + spacing: _margin + Layout.fillWidth: true + + QGCLabel { + text: qsTr("Clockwise (°)") + Layout.preferredWidth: _labelWidth + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + QGCTextField { + id: degreesCWField + text: "0" + Layout.preferredWidth: _textFieldWidth + Layout.fillWidth: true + inputMethodHints: Qt.ImhPreferNumbers + validator: DoubleValidator { + notation: DoubleValidator.StandardNotation + locale: Qt.locale() + } + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + QGCCheckBox { + id: rotateTakeoffCheck + text: qsTr("Also move takeoff items") + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + } + + QGCCheckBox { + id: rotateLandingCheck + text: qsTr("Also move landing items") + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + } + } + + QGCLabel { + Layout.fillWidth: true + Layout.maximumWidth: _labelWidth + _textFieldWidth + + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + + text: qsTr("Note: Complex items are rotated by moving their reference coordinate: their geometry and orientation are not changed.") + } + } + + onAccepted: { + var degreesCW = Number.fromLocaleString(Qt.locale(), degreesCWField.text) + + if (isNaN(degreesCW)) degreesCW = 0 + + _missionController.rotateMission( + degreesCW, + rotateTakeoffCheck.checked, + rotateLandingCheck.checked + ) + } + } + } + + Component { + id: editMissionPositionDialogComponent + + EditPositionDialog { + id: missionEditPositionDialog + + onCoordinateChanged: _missionController.repositionMission(coordinate) + } + } + QGCColoredImage { id: hamburger @@ -281,7 +551,7 @@ Rectangle { height: _hamburgerSize sourceSize.height: _hamburgerSize source: "qrc:/qmlimages/Hamburger.svg" - visible: missionItem.isCurrentItem && missionItem.sequenceNumber !== 0 + visible: missionItem.isCurrentItem color: qgcPal.buttonHighlightText QGCMouseArea { @@ -291,7 +561,11 @@ Rectangle { position = Qt.point(position.x, position.y) // For some strange reason using mainWindow in mapToItem doesn't work, so we use globals.parent instead which also gets us mainWindow position = mapToItem(globals.parent, position) - var dropPanel = hamburgerMenuDropPanelComponent.createObject(mainWindow, { clickRect: Qt.rect(position.x, position.y, 0, 0) }) + + var panelComponent = (missionItem.sequenceNumber === 0) + ? missionStartHamburgerMenuDropPanelComponent + : hamburgerMenuDropPanelComponent + var dropPanel = panelComponent.createObject(mainWindow, { clickRect: Qt.rect(position.x, position.y, 0, 0) }) dropPanel.open() } } From 483da2cc22d9f104f21af71189a918c8b57c270c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Mon, 2 Mar 2026 14:45:06 +0100 Subject: [PATCH 045/119] Fix EditPositionDialog init reposition Opening EditPositionDialog was triggering coordinateChanged during initialization, which then caused mission reposition or item coordinate updates even when nothing actually moved. Ensured initialization does not emit, while keeping explicit user actions behaving the same. Key changes: - Make EditPositionDialogController.coordinate REQUIRED - Do not emit on the initial invalid-to-valid coordinate assignment - Warn and ignore attempts to set an invalid coordinate - Route Geo/UTM/MGRS updates through setCoordinate for behaviour centralization - Set coordinate via QML binding in EditPositionDialog declarations, instead of via createObject property maps (no longer supported) --- .../EditPositionDialogController.cc | 42 ++++++++++++++----- .../EditPositionDialogController.h | 2 +- src/QmlControls/MissionItemEditor.qml | 5 +-- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/QmlControls/EditPositionDialogController.cc b/src/QmlControls/EditPositionDialogController.cc index 57eae01e375e..d26d9a847877 100644 --- a/src/QmlControls/EditPositionDialogController.cc +++ b/src/QmlControls/EditPositionDialogController.cc @@ -40,14 +40,31 @@ EditPositionDialogController::~EditPositionDialogController() void EditPositionDialogController::setCoordinate(QGeoCoordinate coordinate) { - if (coordinate != _coordinate) { - _coordinate = coordinate; - emit coordinateChanged(coordinate); + if (!coordinate.isValid()) { + qCWarning(EditPositionDialogControllerLog) << "Attempt to set invalid coordinate"; + return; + } + + if (coordinate == _coordinate) { + return; + } + + const bool wasInvalid = !_coordinate.isValid(); + _coordinate = coordinate; + + // Do not emit on the initial invalid->valid transition to prevent + // onCoordinateChanged handlers from firing on initial dialog setup. + if (!wasInvalid) { + emit coordinateChanged(_coordinate); } } void EditPositionDialogController::initValues() { + if (!_coordinate.isValid()) { + return; + } + _latitudeFact->setRawValue(_coordinate.latitude()); _longitudeFact->setRawValue(_coordinate.longitude()); @@ -68,17 +85,19 @@ void EditPositionDialogController::initValues() void EditPositionDialogController::setFromGeo() { - _coordinate.setLatitude(_latitudeFact->rawValue().toDouble()); - _coordinate.setLongitude(_longitudeFact->rawValue().toDouble()); - emit coordinateChanged(_coordinate); + QGeoCoordinate newCoordinate = _coordinate; + newCoordinate.setLatitude(_latitudeFact->rawValue().toDouble()); + newCoordinate.setLongitude(_longitudeFact->rawValue().toDouble()); + setCoordinate(newCoordinate); } void EditPositionDialogController::setFromUTM() { qCDebug(EditPositionDialogControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1); - if (QGCGeo::convertUTMToGeo(_eastingFact->rawValue().toDouble(), _northingFact->rawValue().toDouble(), _zoneFact->rawValue().toInt(), _hemisphereFact->rawValue().toInt() == 1, _coordinate)) { - qCDebug(EditPositionDialogControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1) << _coordinate; - emit coordinateChanged(_coordinate); + QGeoCoordinate newCoordinate; + if (QGCGeo::convertUTMToGeo(_eastingFact->rawValue().toDouble(), _northingFact->rawValue().toDouble(), _zoneFact->rawValue().toInt(), _hemisphereFact->rawValue().toInt() == 1, newCoordinate)) { + qCDebug(EditPositionDialogControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1) << newCoordinate; + setCoordinate(newCoordinate); } else { initValues(); } @@ -86,8 +105,9 @@ void EditPositionDialogController::setFromUTM() void EditPositionDialogController::setFromMGRS() { - if (QGCGeo::convertMGRSToGeo(_mgrsFact->rawValue().toString(), _coordinate)) { - emit coordinateChanged(_coordinate); + QGeoCoordinate newCoordinate; + if (QGCGeo::convertMGRSToGeo(_mgrsFact->rawValue().toString(), newCoordinate)) { + setCoordinate(newCoordinate); } else { initValues(); } diff --git a/src/QmlControls/EditPositionDialogController.h b/src/QmlControls/EditPositionDialogController.h index 6d4cabf878b0..ef56cac76468 100644 --- a/src/QmlControls/EditPositionDialogController.h +++ b/src/QmlControls/EditPositionDialogController.h @@ -13,7 +13,7 @@ class EditPositionDialogController : public QObject { Q_OBJECT QML_ELEMENT - Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged) + Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged REQUIRED) Q_PROPERTY(Fact *latitude READ latitude CONSTANT) Q_PROPERTY(Fact *longitude READ longitude CONSTANT) Q_PROPERTY(Fact *zone READ zone CONSTANT) diff --git a/src/QmlControls/MissionItemEditor.qml b/src/QmlControls/MissionItemEditor.qml index 8c08c08536e2..97b7b168e5b3 100644 --- a/src/QmlControls/MissionItemEditor.qml +++ b/src/QmlControls/MissionItemEditor.qml @@ -308,9 +308,7 @@ Rectangle { text: qsTr("Edit mission position...") enabled: _missionController.plannedHomePosition.isValid onClicked: { - editMissionPositionDialogComponent.createObject( - mainWindow, - { coordinate: _missionController.plannedHomePosition }).open() + editMissionPositionDialogComponent.createObject(mainWindow).open() missionStartHamburgerMenuDropPanel.close() } } @@ -537,6 +535,7 @@ Rectangle { EditPositionDialog { id: missionEditPositionDialog + coordinate: _missionController.plannedHomePosition onCoordinateChanged: _missionController.repositionMission(coordinate) } } From 563eec59f10a93ab404fce07957393fa216759f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Alonso?= Date: Mon, 2 Mar 2026 14:45:40 +0100 Subject: [PATCH 046/119] Add mission item bulk displacement unit tests The 2 tests cover a valid transform case (reposition + vertical offset) and an invalid-home no-op case. --- test/MissionManager/MissionControllerTest.cc | 162 +++++++++++++++++++ test/MissionManager/MissionControllerTest.h | 4 + 2 files changed, 166 insertions(+) diff --git a/test/MissionManager/MissionControllerTest.cc b/test/MissionManager/MissionControllerTest.cc index 410f6071be1f..06bad0b6fb8e 100644 --- a/test/MissionManager/MissionControllerTest.cc +++ b/test/MissionManager/MissionControllerTest.cc @@ -12,6 +12,13 @@ using namespace TestFixtures; MissionControllerTest::~MissionControllerTest() = default; +namespace { +// Coordinate checks involve geodesic reconstruction so they use 0.5 m. Altitude +// checks are direct arithmetic, so they use a much tighter 1e-4 m tolerance. +constexpr double kCoordToleranceMeters = 0.5; +constexpr double kAltToleranceMeters = 1e-4; +} // namespace + void MissionControllerTest::cleanup() { _masterController.reset(); @@ -187,6 +194,161 @@ void MissionControllerTest::_testVehicleYawRecalc() } } +void MissionControllerTest::_testMissionReposition() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + + MissionSettingsItem* settingsItem = _missionController->visualItems()->value(0); + QVERIFY(settingsItem); + + const QGeoCoordinate home = Coord::zurich(); + settingsItem->setInitialHomePositionFromUser(home); + + const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(150.0, 15.0); + const QGeoCoordinate wp2 = home.atDistanceAndAzimuth(320.0, 120.0); + _missionController->insertSimpleMissionItem(wp1, 1); + _missionController->insertSimpleMissionItem(wp2, 2); + + SimpleMissionItem* item1 = _missionController->visualItems()->value(1); + SimpleMissionItem* item2 = _missionController->visualItems()->value(2); + QVERIFY(item1); + QVERIFY(item2); + + const double oldHomeAlt = settingsItem->editableAlt(); + const double oldAlt1 = item1->editableAlt(); + const double oldAlt2 = item2->editableAlt(); + + const QGeoCoordinate newHome = home.atDistanceAndAzimuth(200.0, 65.0); + const QGeoCoordinate expectedWp1 = + newHome.atDistanceAndAzimuth(home.distanceTo(wp1), home.azimuthTo(wp1)); + const QGeoCoordinate expectedWp2 = + newHome.atDistanceAndAzimuth(home.distanceTo(wp2), home.azimuthTo(wp2)); + _missionController->repositionMission(newHome, true, true); + QVERIFY_TRUE_WAIT((settingsItem->coordinate().distanceTo(newHome) <= kCoordToleranceMeters) && + (item1->coordinate().distanceTo(expectedWp1) <= kCoordToleranceMeters) && + (item2->coordinate().distanceTo(expectedWp2) <= kCoordToleranceMeters), + TestTimeout::shortMs()); + + QCOMPARE_COORDS(settingsItem->coordinate(), newHome, kCoordToleranceMeters); + QCOMPARE_COORDS(item1->coordinate(), expectedWp1, kCoordToleranceMeters); + QCOMPARE_COORDS(item2->coordinate(), expectedWp2, kCoordToleranceMeters); + QCOMPARE_FUZZY(settingsItem->editableAlt(), oldHomeAlt, kAltToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldAlt1, kAltToleranceMeters); + QCOMPARE_FUZZY(item2->editableAlt(), oldAlt2, kAltToleranceMeters); +} + +void MissionControllerTest::_testMissionOffset() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + + MissionSettingsItem* settingsItem = _missionController->visualItems()->value(0); + QVERIFY(settingsItem); + + const QGeoCoordinate home = Coord::zurich(); + settingsItem->setInitialHomePositionFromUser(home); + + const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(150.0, 15.0); + const QGeoCoordinate wp2 = home.atDistanceAndAzimuth(320.0, 120.0); + _missionController->insertSimpleMissionItem(wp1, 1); + _missionController->insertSimpleMissionItem(wp2, 2); + + SimpleMissionItem* item1 = _missionController->visualItems()->value(1); + SimpleMissionItem* item2 = _missionController->visualItems()->value(2); + QVERIFY(item1); + QVERIFY(item2); + + const double oldHomeAlt = settingsItem->editableAlt(); + const double oldAlt1 = item1->editableAlt(); + const double oldAlt2 = item2->editableAlt(); + + _missionController->offsetMission(0.0, 0.0, 12.0, true, true); + QVERIFY_TRUE_WAIT(qAbs(settingsItem->editableAlt() - oldHomeAlt) <= kAltToleranceMeters && + qAbs(item1->editableAlt() - (oldAlt1 + 12.0)) <= kAltToleranceMeters && + qAbs(item2->editableAlt() - (oldAlt2 + 12.0)) <= kAltToleranceMeters, + TestTimeout::shortMs()); + + QCOMPARE_FUZZY(settingsItem->editableAlt(), oldHomeAlt, kAltToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldAlt1 + 12.0, kAltToleranceMeters); + QCOMPARE_FUZZY(item2->editableAlt(), oldAlt2 + 12.0, kAltToleranceMeters); +} + +void MissionControllerTest::_testMissionRotate() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + + MissionSettingsItem* settingsItem = _missionController->visualItems()->value(0); + QVERIFY(settingsItem); + + const QGeoCoordinate home = Coord::zurich(); + settingsItem->setInitialHomePositionFromUser(home); + + const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(180.0, 20.0); + const QGeoCoordinate wp2 = home.atDistanceAndAzimuth(260.0, 135.0); + _missionController->insertSimpleMissionItem(wp1, 1); + _missionController->insertSimpleMissionItem(wp2, 2); + + SimpleMissionItem* item1 = _missionController->visualItems()->value(1); + SimpleMissionItem* item2 = _missionController->visualItems()->value(2); + QVERIFY(item1); + QVERIFY(item2); + + const double oldHomeAlt = settingsItem->editableAlt(); + const double oldAlt1 = item1->editableAlt(); + const double oldAlt2 = item2->editableAlt(); + + const QGeoCoordinate expectedWp1 = + home.atDistanceAndAzimuth(home.distanceTo(wp1), home.azimuthTo(wp1) + 90.0); + const QGeoCoordinate expectedWp2 = + home.atDistanceAndAzimuth(home.distanceTo(wp2), home.azimuthTo(wp2) + 90.0); + + _missionController->rotateMission(90.0, true, true); + QVERIFY_TRUE_WAIT((settingsItem->coordinate().distanceTo(home) <= kCoordToleranceMeters) && + (item1->coordinate().distanceTo(expectedWp1) <= kCoordToleranceMeters) && + (item2->coordinate().distanceTo(expectedWp2) <= kCoordToleranceMeters), + TestTimeout::shortMs()); + + QCOMPARE_COORDS(settingsItem->coordinate(), home, kCoordToleranceMeters); + QCOMPARE_COORDS(item1->coordinate(), expectedWp1, kCoordToleranceMeters); + QCOMPARE_COORDS(item2->coordinate(), expectedWp2, kCoordToleranceMeters); + QCOMPARE_FUZZY(settingsItem->editableAlt(), oldHomeAlt, kAltToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldAlt1, kAltToleranceMeters); + QCOMPARE_FUZZY(item2->editableAlt(), oldAlt2, kAltToleranceMeters); +} + +void MissionControllerTest::_testMissionTransformsInvalidHome() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + + MissionSettingsItem* settingsItem = + _missionController->visualItems()->value(0); + QVERIFY(settingsItem); + + const QGeoCoordinate home = Coord::zurich(); + settingsItem->setInitialHomePositionFromUser(home); + + const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(180.0, 45.0); + _missionController->insertSimpleMissionItem(wp1, 1); + + SimpleMissionItem* item1 = _missionController->visualItems()->value(1); + QVERIFY(item1); + + const QGeoCoordinate oldItemCoord = item1->coordinate(); + const double oldItemAlt = item1->editableAlt(); + + settingsItem->setCoordinate(QGeoCoordinate()); + QVERIFY_TRUE_WAIT(!settingsItem->coordinate().isValid(), TestTimeout::shortMs()); + + _missionController->repositionMission(home.atDistanceAndAzimuth(100.0, 0.0), true, true); + _missionController->offsetMission(10.0, 5.0, 8.0, true, true); + _missionController->rotateMission(45.0, true, true); + QVERIFY_TRUE_WAIT((item1->coordinate().distanceTo(oldItemCoord) <= kCoordToleranceMeters) && + (qAbs(item1->editableAlt() - oldItemAlt) <= kAltToleranceMeters), + TestTimeout::shortMs()); + + QCOMPARE_COORDS(item1->coordinate(), oldItemCoord, kCoordToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldItemAlt, kAltToleranceMeters); +} + void MissionControllerTest::_testLoadJsonSectionAvailable() { _initForFirmwareType(MAV_AUTOPILOT_PX4); diff --git a/test/MissionManager/MissionControllerTest.h b/test/MissionManager/MissionControllerTest.h index 53a1f611434e..3f4432eaa89d 100644 --- a/test/MissionManager/MissionControllerTest.h +++ b/test/MissionManager/MissionControllerTest.h @@ -23,6 +23,10 @@ private slots: void _testGlobalAltMode(); void _testGimbalRecalc(); void _testVehicleYawRecalc(); + void _testMissionReposition(); + void _testMissionOffset(); + void _testMissionRotate(); + void _testMissionTransformsInvalidHome(); // Parameterized tests - runs once per autopilot type UT_PARAMETERIZED_TEST(_testEmptyVehicle); From b05493208e94f682dc9f319a5e3182e1851988b7 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 7 Mar 2026 11:25:12 -0800 Subject: [PATCH 047/119] Refactor: Extract vehicle support properties into VehicleSupports class Move all supports* and *Supported Q_PROPERTYs from Vehicle into a new VehicleSupports class, accessible via vehicle.supports in QML and vehicle->supports() in C++. Properties moved and renamed for clarity: - supportsThrottleModeCenterZero -> supports.throttleModeCenterZero - supportsNegativeThrust -> supports.negativeThrust - supportsJSButton -> supports.jsButton - supportsRadio -> supports.radio - supportsMotorInterference -> supports.motorInterference - supportsSmartRTL -> supports.smartRTL - supportsTerrainFrame -> supports.terrainFrame - guidedModeSupported -> supports.guidedMode - pauseVehicleSupported -> supports.pauseVehicle - orbitModeSupported -> supports.orbitMode - roiModeSupported -> supports.roiMode - changeHeadingSupported -> supports.changeHeading Takeoff capabilities refactored to separate mission planning from guided actions: - takeoffVehicleSupported -> supports.takeoffMissionCommand (planning) - guidedTakeoffSupported -> supports.guidedTakeoffWithAltitude (guided) - New: supports.guidedTakeoffWithoutAltitude (guided, no altitude) --- .../APM/APMAutoPilotPlugin.cc | 3 +- .../APM/APMSensorsComponent.qml | 2 +- src/FlyView/GuidedActionsController.qml | 28 +++--- src/FlyView/MultiVehicleList.qml | 2 +- src/Joystick/Joystick.cc | 5 +- src/MissionManager/MissionController.cc | 3 +- src/QmlControls/AltModeCombo.qml | 2 +- src/QmlControls/MissionItemTreeView.qml | 4 +- src/QmlControls/PlanView.qml | 2 +- .../TransectStyleComplexItemTerrainFollow.qml | 2 +- src/UI/toolbar/RCRSSIIndicator.qml | 2 +- src/Vehicle/CMakeLists.txt | 2 + src/Vehicle/Vehicle.cc | 96 +++---------------- src/Vehicle/Vehicle.h | 35 ++----- .../VehicleSetup/JoystickComponentButtons.qml | 8 +- .../JoystickComponentSettings.qml | 2 +- src/Vehicle/VehicleSupports.cc | 88 +++++++++++++++++ src/Vehicle/VehicleSupports.h | 55 +++++++++++ 18 files changed, 200 insertions(+), 141 deletions(-) create mode 100644 src/Vehicle/VehicleSupports.cc create mode 100644 src/Vehicle/VehicleSupports.h diff --git a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc index cfb238e42d25..8c742440a015 100644 --- a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc +++ b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc @@ -19,6 +19,7 @@ #include "QGCApplication.h" #include "QGCLoggingCategory.h" #include "Vehicle.h" +#include "VehicleSupports.h" #include "VehicleComponent.h" #ifdef QT_DEBUG #include "APMFollowComponent.h" @@ -55,7 +56,7 @@ const QVariantList &APMAutoPilotPlugin::vehicleComponents() _airframeComponent->setupTriggerSignals(); _components.append(QVariant::fromValue(qobject_cast(_airframeComponent))); - if (_vehicle->supportsRadio()) { + if (_vehicle->supports()->radio()) { _radioComponent = new APMRadioComponent(_vehicle, this); _radioComponent->setupTriggerSignals(); _components.append(QVariant::fromValue(qobject_cast(_radioComponent))); diff --git a/src/AutoPilotPlugins/APM/APMSensorsComponent.qml b/src/AutoPilotPlugins/APM/APMSensorsComponent.qml index 644bcd0f553b..a7af794599d8 100644 --- a/src/AutoPilotPlugins/APM/APMSensorsComponent.qml +++ b/src/AutoPilotPlugins/APM/APMSensorsComponent.qml @@ -738,7 +738,7 @@ SetupPage { QGCButton { width: _buttonWidth text: qsTr("CompassMot") - visible: globals.activeVehicle ? globals.activeVehicle.supportsMotorInterference : false + visible: globals.activeVehicle ? globals.activeVehicle.supports.motorInterference : false onClicked: compassMotDialogFactory.open() } diff --git a/src/FlyView/GuidedActionsController.qml b/src/FlyView/GuidedActionsController.qml index 1909f25d01c2..87394adb9b84 100644 --- a/src/FlyView/GuidedActionsController.qml +++ b/src/FlyView/GuidedActionsController.qml @@ -125,15 +125,15 @@ Item { property bool showArm: _guidedActionsEnabled && !_vehicleArmed && _canArm property bool showForceArm: _guidedActionsEnabled && !_vehicleArmed property bool showDisarm: _guidedActionsEnabled && _vehicleArmed && !_vehicleFlying - property bool showRTL: _guidedActionsEnabled && _vehicleArmed && _activeVehicle.guidedModeSupported && _vehicleFlying && !_vehicleInRTLMode - property bool showTakeoff: _guidedActionsEnabled && _activeVehicle.takeoffVehicleSupported && !_vehicleFlying && _canTakeoff - property bool showLand: _guidedActionsEnabled && _activeVehicle.guidedModeSupported && _vehicleArmed && !_activeVehicle.fixedWing && !_vehicleInLandMode + property bool showRTL: _guidedActionsEnabled && _vehicleArmed && _activeVehicle.supports.guidedMode && _vehicleFlying && !_vehicleInRTLMode + property bool showTakeoff: _guidedActionsEnabled && (_activeVehicle.supports.guidedTakeoffWithAltitude || _activeVehicle.supports.guidedTakeoffWithoutAltitude) && !_vehicleFlying && _canTakeoff + property bool showLand: _guidedActionsEnabled && _activeVehicle.supports.guidedMode && _vehicleArmed && !_activeVehicle.fixedWing && !_vehicleInLandMode property bool showStartMission: _guidedActionsEnabled && _missionAvailable && !_missionActive && !_vehicleFlying && _canStartMission property bool showContinueMission: _guidedActionsEnabled && _missionAvailable && !_missionActive && _vehicleArmed && _vehicleFlying && (_currentMissionIndex < _missionItemCount - 1) - property bool showPause: _guidedActionsEnabled && _vehicleArmed && _activeVehicle.pauseVehicleSupported && _vehicleFlying && !_vehiclePaused && !_fixedWingOnApproach - property bool showChangeAlt: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.guidedModeSupported && _vehicleArmed && !_missionActive - property bool showChangeLoiterRadius: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.guidedModeSupported && _vehicleArmed && !_missionActive && _vehicleInFwdFlight && fwdFlightGotoMapCircle.visible - property bool showChangeSpeed: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.guidedModeSupported && _vehicleArmed && !_missionActive && _speedLimitsAvailable + property bool showPause: _guidedActionsEnabled && _vehicleArmed && _activeVehicle.supports.pauseVehicle && _vehicleFlying && !_vehiclePaused && !_fixedWingOnApproach + property bool showChangeAlt: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.supports.guidedMode && _vehicleArmed && !_missionActive + property bool showChangeLoiterRadius: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.supports.guidedMode && _vehicleArmed && !_missionActive && _vehicleInFwdFlight && fwdFlightGotoMapCircle.visible + property bool showChangeSpeed: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.supports.guidedMode && _vehicleArmed && !_missionActive && _speedLimitsAvailable property bool showOrbit: _guidedActionsEnabled && _vehicleFlying && __orbitSupported && !_missionActive && _activeVehicle.homePosition.isValid && !isNaN(_activeVehicle.homePosition.altitude) property bool showROI: _guidedActionsEnabled && _vehicleFlying && __roiSupported property bool showLandAbort: _guidedActionsEnabled && _vehicleFlying && _fixedWingOnApproach @@ -176,10 +176,10 @@ Item { property bool _speedLimitsAvailable: _activeVehicle && ((_vehicleInFwdFlight && _activeVehicle.haveFWSpeedLimits) || (!_vehicleInFwdFlight && _activeVehicle.haveMRSpeedLimits)) // You can turn on log output for GuidedActionsController by turning on GuidedActionsControllerLog category - property bool __guidedModeSupported: _activeVehicle ? _activeVehicle.guidedModeSupported : false - property bool __pauseVehicleSupported: _activeVehicle ? _activeVehicle.pauseVehicleSupported : false - property bool __roiSupported: _activeVehicle ? !_hideROI && _activeVehicle.roiModeSupported : false - property bool __orbitSupported: _activeVehicle ? !_hideOrbit && _activeVehicle.orbitModeSupported : false + property bool __guidedModeSupported: _activeVehicle ? _activeVehicle.supports.guidedMode : false + property bool __pauseVehicleSupported: _activeVehicle ? _activeVehicle.supports.pauseVehicle : false + property bool __roiSupported: _activeVehicle ? !_hideROI && _activeVehicle.supports.roiMode : false + property bool __orbitSupported: _activeVehicle ? !_hideOrbit && _activeVehicle.supports.orbitMode : false property bool __flightMode: _flightMode // Allow custom builds to add custom actions by overriding CustomGuidedActionsController.qml @@ -426,7 +426,7 @@ Item { confirmDialog.title = takeoffTitle confirmDialog.message = takeoffMessage confirmDialog.hideTrigger = Qt.binding(function() { return !showTakeoff }) - guidedValueSlider.visible = _activeVehicle.guidedTakeoffSupported + guidedValueSlider.visible = _activeVehicle.supports.guidedTakeoffWithAltitude break; case actionStartMission: showImmediate = false @@ -461,7 +461,7 @@ Item { case actionRTL: confirmDialog.title = rtlTitle confirmDialog.message = rtlMessage - if (_activeVehicle.supportsSmartRTL) { + if (_activeVehicle.supports.smartRTL) { confirmDialog.optionText = qsTr("Smart RTL") confirmDialog.optionChecked = false } @@ -560,7 +560,7 @@ Item { _activeVehicle.guidedModeLand() break case actionTakeoff: - if (_activeVehicle.guidedTakeoffSupported) { + if (_activeVehicle.supports.guidedTakeoffWithAltitude) { var valueInMeters = _unitsConversion.appSettingsVerticalDistanceUnitsToMeters(sliderOutputValue) _activeVehicle.guidedModeTakeoff(valueInMeters) } else { diff --git a/src/FlyView/MultiVehicleList.qml b/src/FlyView/MultiVehicleList.qml index 3defc3378eb1..43e25cf07cb6 100644 --- a/src/FlyView/MultiVehicleList.qml +++ b/src/FlyView/MultiVehicleList.qml @@ -51,7 +51,7 @@ Item { function pauseAvailable() { for (var i = 0; i < selectedVehicles.count; i++) { var vehicle = selectedVehicles.get(i) - if (vehicle.armed === true && vehicle.pauseVehicleSupported) { + if (vehicle.armed === true && vehicle.supports.pauseVehicle) { return true } } diff --git a/src/Joystick/Joystick.cc b/src/Joystick/Joystick.cc index b7e996aa7769..83f6409e7ba4 100644 --- a/src/Joystick/Joystick.cc +++ b/src/Joystick/Joystick.cc @@ -9,6 +9,7 @@ #include "QmlObjectListModel.h" #include "SettingsManager.h" #include "Vehicle.h" +#include "VehicleSupports.h" #include "JoystickManager.h" #include "MultiVehicleManager.h" @@ -1001,8 +1002,8 @@ void Joystick::_handleAxis() } // Adjust throttle to 0:1 range - if (throttleModeCenterZero && vehicle->supportsThrottleModeCenterZero()) { - if (!vehicle->supportsNegativeThrust() || !negativeThrust) { + if (throttleModeCenterZero && vehicle->supports()->throttleModeCenterZero()) { + if (!vehicle->supports()->negativeThrust() || !negativeThrust) { throttle = std::max(0.0f, throttle); } } else { diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 769fc0a3fd0d..b9a2eec92d09 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -1,5 +1,6 @@ #include "MissionController.h" #include "Vehicle.h" +#include "VehicleSupports.h" #include "MissionManager.h" #include "FlightPathSegment.h" #include "FirmwarePlugin.h" @@ -2521,7 +2522,7 @@ void MissionController::setCurrentPlanViewSeqNum(int sequenceNumber, bool force) _previousCoordinate = QGeoCoordinate(); bool noItemsAddedYet = _visualItems->count() == 1; - if (_masterController->controllerVehicle()->takeoffVehicleSupported() && !_planViewSettings->takeoffItemNotRequired()->rawValue().toBool() && noItemsAddedYet) { + if (_masterController->controllerVehicle()->supports()->takeoffMissionCommand() && !_planViewSettings->takeoffItemNotRequired()->rawValue().toBool() && noItemsAddedYet) { _onlyInsertTakeoffValid = true; } diff --git a/src/QmlControls/AltModeCombo.qml b/src/QmlControls/AltModeCombo.qml index 862bf184e622..83a7f79424d2 100644 --- a/src/QmlControls/AltModeCombo.qml +++ b/src/QmlControls/AltModeCombo.qml @@ -41,7 +41,7 @@ QGCComboBox { if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude && altitudeMode != QGroundControl.AltitudeModeAbsolute) { removeModes.push(QGroundControl.AltitudeModeAbsolute) } - if (!vehicle.supportsTerrainFrame) { + if (!vehicle.supports.terrainFrame) { removeModes.push(QGroundControl.AltitudeModeTerrainFrame) } diff --git a/src/QmlControls/MissionItemTreeView.qml b/src/QmlControls/MissionItemTreeView.qml index f23e7181399b..6597a7c634c2 100644 --- a/src/QmlControls/MissionItemTreeView.qml +++ b/src/QmlControls/MissionItemTreeView.qml @@ -315,7 +315,7 @@ TreeView { Connections { target: defaultsRect._controllerVehicle function onFirmwareTypeChanged() { - if (!defaultsRect._controllerVehicle.supportsTerrainFrame + if (!defaultsRect._controllerVehicle.supports.terrainFrame && defaultsRect._missionController.globalAltitudeMode === QGroundControl.AltitudeModeTerrainFrame) { defaultsRect._missionController.globalAltitudeMode = QGroundControl.AltitudeModeCalcAboveTerrain } @@ -345,7 +345,7 @@ TreeView { onClicked: { let removeModes = [] let updateFunction = function(altMode) { defaultsRect._missionController.globalAltitudeMode = altMode } - if (!defaultsRect._controllerVehicle.supportsTerrainFrame) { + if (!defaultsRect._controllerVehicle.supports.terrainFrame) { removeModes.push(QGroundControl.AltitudeModeTerrainFrame) } if (!defaultsRect._noMissionItemsAdded) { diff --git a/src/QmlControls/PlanView.qml b/src/QmlControls/PlanView.qml index ac9db0c581e3..9d543d56c40e 100644 --- a/src/QmlControls/PlanView.qml +++ b/src/QmlControls/PlanView.qml @@ -454,7 +454,7 @@ Item { id: roiButton text: qsTr("ROI") iconSource: "/qmlimages/roi.svg" - visible: toolStrip._isMissionLayer && _planMasterController.controllerVehicle.roiModeSupported + visible: toolStrip._isMissionLayer && _planMasterController.controllerVehicle.supports.roiMode checkable: true onTriggered: { _addROIOnClick = !_addROIOnClick; if (_addROIOnClick) _addWaypointOnClick = false } }, diff --git a/src/QmlControls/TransectStyleComplexItemTerrainFollow.qml b/src/QmlControls/TransectStyleComplexItemTerrainFollow.qml index 66c034f82a5a..de1872f6a353 100644 --- a/src/QmlControls/TransectStyleComplexItemTerrainFollow.qml +++ b/src/QmlControls/TransectStyleComplexItemTerrainFollow.qml @@ -20,7 +20,7 @@ ColumnLayout { var removeModes = [] var updateFunction = function(altMode){ missionItem.cameraCalc.distanceMode = altMode } removeModes.push(QGroundControl.AltitudeModeMixed) - if (!missionItem.masterController.controllerVehicle.supportsTerrainFrame) { + if (!missionItem.masterController.controllerVehicle.supports.terrainFrame) { removeModes.push(QGroundControl.AltitudeModeTerrainFrame) } if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude || !_missionItem.cameraCalc.isManualCamera) { diff --git a/src/UI/toolbar/RCRSSIIndicator.qml b/src/UI/toolbar/RCRSSIIndicator.qml index a310fa5a0640..44b037e32fb2 100644 --- a/src/UI/toolbar/RCRSSIIndicator.qml +++ b/src/UI/toolbar/RCRSSIIndicator.qml @@ -12,7 +12,7 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom - property bool showIndicator: _activeVehicle.supportsRadio && _rcRSSIAvailable + property bool showIndicator: _activeVehicle.supports.radio && _rcRSSIAvailable property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle property bool _rcRSSIAvailable: _activeVehicle.rcRSSI > 0 && _activeVehicle.rcRSSI <= 100 diff --git a/src/Vehicle/CMakeLists.txt b/src/Vehicle/CMakeLists.txt index 93d8016584d6..1da3b700f5f0 100644 --- a/src/Vehicle/CMakeLists.txt +++ b/src/Vehicle/CMakeLists.txt @@ -31,6 +31,8 @@ target_sources(${CMAKE_PROJECT_NAME} VehicleLinkManager.h VehicleObjectAvoidance.cc VehicleObjectAvoidance.h + VehicleSupports.cc + VehicleSupports.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index 2b531563eafd..e0a28657a14c 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -1,5 +1,6 @@ #include "Vehicle.h" #include "Actuators.h" +#include "VehicleSupports.h" #include "ADSBVehicleManager.h" #include "AudioOutput.h" #include "AutoPilotPlugin.h" @@ -342,6 +343,7 @@ void Vehicle::_commonInit(LinkInterface* link) } _gimbalController = new GimbalController(this); + _vehicleSupports = new VehicleSupports(this); _createCameraManager(); } @@ -1869,36 +1871,6 @@ bool Vehicle::vtol() const return QGCMAVLink::isVTOL(vehicleType()); } -bool Vehicle::supportsThrottleModeCenterZero() const -{ - return _firmwarePlugin->supportsThrottleModeCenterZero(); -} - -bool Vehicle::supportsNegativeThrust() -{ - return _firmwarePlugin->supportsNegativeThrust(this); -} - -bool Vehicle::supportsRadio() const -{ - return _firmwarePlugin->supportsRadio(); -} - -bool Vehicle::supportsJSButton() const -{ - return _firmwarePlugin->supportsJSButton(); -} - -bool Vehicle::supportsMotorInterference() const -{ - return _firmwarePlugin->supportsMotorInterference(); -} - -bool Vehicle::supportsTerrainFrame() const -{ - return !px4Firmware(); -} - QString Vehicle::vehicleTypeString() const { return QGCMAVLink::mavTypeToString(_vehicleType); @@ -1951,41 +1923,6 @@ void Vehicle::_setLanding(bool landing) } } -bool Vehicle::guidedModeSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::GuidedModeCapability); -} - -bool Vehicle::pauseVehicleSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::PauseVehicleCapability); -} - -bool Vehicle::orbitModeSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::OrbitModeCapability); -} - -bool Vehicle::roiModeSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::ROIModeCapability); -} - -bool Vehicle::takeoffVehicleSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::TakeoffVehicleCapability); -} - -bool Vehicle::guidedTakeoffSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::GuidedTakeoffCapability); -} - -bool Vehicle::changeHeadingSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::ChangeHeadingCapability); -} - QString Vehicle::gotoFlightMode() const { return _firmwarePlugin->gotoFlightMode(); @@ -1993,7 +1930,7 @@ QString Vehicle::gotoFlightMode() const void Vehicle::guidedModeRTL(bool smartRTL) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2002,7 +1939,7 @@ void Vehicle::guidedModeRTL(bool smartRTL) void Vehicle::guidedModeLand() { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2011,7 +1948,7 @@ void Vehicle::guidedModeLand() void Vehicle::guidedModeTakeoff(double altitudeRelative) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2058,7 +1995,7 @@ void Vehicle::startMission() void Vehicle::guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2075,7 +2012,7 @@ void Vehicle::guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double for void Vehicle::guidedModeChangeAltitude(double altitudeChange, bool pauseVehicle) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2085,7 +2022,7 @@ void Vehicle::guidedModeChangeAltitude(double altitudeChange, bool pauseVehicle) void Vehicle::guidedModeChangeGroundSpeedMetersSecond(double groundspeed) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2095,7 +2032,7 @@ Vehicle::guidedModeChangeGroundSpeedMetersSecond(double groundspeed) void Vehicle::guidedModeChangeEquivalentAirspeedMetersSecond(double airspeed) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2104,7 +2041,7 @@ Vehicle::guidedModeChangeEquivalentAirspeedMetersSecond(double airspeed) void Vehicle::guidedModeOrbit(const QGeoCoordinate& centerCoord, double radius, double amslAltitude) { - if (!orbitModeSupported()) { + if (!_vehicleSupports->orbitMode()) { qgcApp()->showAppMessage(QStringLiteral("Orbit mode not supported by Vehicle.")); return; } @@ -2139,7 +2076,7 @@ void Vehicle::guidedModeROI(const QGeoCoordinate& centerCoord) if (!centerCoord.isValid()) { return; } - if (!roiModeSupported()) { + if (!_vehicleSupports->roiMode()) { qgcApp()->showAppMessage(QStringLiteral("ROI mode not supported by Vehicle.")); return; } @@ -2228,7 +2165,7 @@ void Vehicle::_sendROICommand(const QGeoCoordinate& coord, MAV_FRAME frame, floa void Vehicle::stopGuidedModeROI() { - if (!roiModeSupported()) { + if (!_vehicleSupports->roiMode()) { qgcApp()->showAppMessage(QStringLiteral("ROI mode not supported by Vehicle.")); return; } @@ -2262,7 +2199,7 @@ void Vehicle::stopGuidedModeROI() void Vehicle::guidedModeChangeHeading(const QGeoCoordinate &headingCoord) { - if (!changeHeadingSupported()) { + if (!_vehicleSupports->changeHeading()) { qgcApp()->showAppMessage(tr("Change Heading not supported by Vehicle.")); return; } @@ -2272,7 +2209,7 @@ void Vehicle::guidedModeChangeHeading(const QGeoCoordinate &headingCoord) void Vehicle::pauseVehicle() { - if (!pauseVehicleSupported()) { + if (!_vehicleSupports->pauseVehicle()) { qgcApp()->showAppMessage(QStringLiteral("Pause not supported by vehicle.")); return; } @@ -3516,11 +3453,6 @@ QString Vehicle::smartRTLFlightMode() const return _firmwarePlugin->smartRTLFlightMode(); } -bool Vehicle::supportsSmartRTL() const -{ - return _firmwarePlugin->supportsSmartRTL(); -} - QString Vehicle::landFlightMode() const { return _firmwarePlugin->landFlightMode(); diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index 3061791f93f2..404fc704c929 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -68,6 +68,7 @@ class TerrainAtCoordinateQuery; class TerrainProtocolHandler; class TrajectoryPoints; class VehicleObjectAvoidance; +class VehicleSupports; namespace events { namespace parser { @@ -91,6 +92,7 @@ class Vehicle : public VehicleFactGroup Q_MOC_INCLUDE("Actuators.h") Q_MOC_INCLUDE("MAVLinkLogManager.h") Q_MOC_INCLUDE("LinkInterface.h") + Q_MOC_INCLUDE("VehicleSupports.h") friend class InitialConnectStateMachine; friend class VehicleLinkManager; @@ -159,11 +161,7 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(bool vtol READ vtol NOTIFY vehicleTypeChanged) Q_PROPERTY(bool rover READ rover NOTIFY vehicleTypeChanged) Q_PROPERTY(bool sub READ sub NOTIFY vehicleTypeChanged) - Q_PROPERTY(bool supportsThrottleModeCenterZero READ supportsThrottleModeCenterZero CONSTANT) - Q_PROPERTY(bool supportsNegativeThrust READ supportsNegativeThrust CONSTANT) - Q_PROPERTY(bool supportsJSButton READ supportsJSButton CONSTANT) - Q_PROPERTY(bool supportsRadio READ supportsRadio CONSTANT) - Q_PROPERTY(bool supportsMotorInterference READ supportsMotorInterference CONSTANT) + Q_PROPERTY(VehicleSupports* supports READ supports CONSTANT) Q_PROPERTY(QString prearmError READ prearmError WRITE setPrearmError NOTIFY prearmErrorChanged) Q_PROPERTY(int motorCount READ motorCount CONSTANT) Q_PROPERTY(bool coaxialMotors READ coaxialMotors CONSTANT) @@ -179,7 +177,6 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(QString pauseFlightMode READ pauseFlightMode CONSTANT) Q_PROPERTY(QString rtlFlightMode READ rtlFlightMode CONSTANT) Q_PROPERTY(QString smartRTLFlightMode READ smartRTLFlightMode CONSTANT) - Q_PROPERTY(bool supportsSmartRTL READ supportsSmartRTL CONSTANT) Q_PROPERTY(QString landFlightMode READ landFlightMode CONSTANT) Q_PROPERTY(QString takeControlFlightMode READ takeControlFlightMode CONSTANT) Q_PROPERTY(QString followFlightMode READ followFlightMode CONSTANT) @@ -201,7 +198,6 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(QString hobbsMeter READ hobbsMeter NOTIFY hobbsMeterChanged) Q_PROPERTY(bool inFwdFlight READ inFwdFlight NOTIFY inFwdFlightChanged) Q_PROPERTY(bool vtolInFwdFlight READ vtolInFwdFlight WRITE setVtolInFwdFlight NOTIFY vtolInFwdFlightChanged) - Q_PROPERTY(bool supportsTerrainFrame READ supportsTerrainFrame NOTIFY firmwareTypeChanged) Q_PROPERTY(quint64 mavlinkSentCount READ mavlinkSentCount NOTIFY mavlinkStatusChanged) Q_PROPERTY(quint64 mavlinkReceivedCount READ mavlinkReceivedCount NOTIFY mavlinkStatusChanged) Q_PROPERTY(quint64 mavlinkLossCount READ mavlinkLossCount NOTIFY mavlinkStatusChanged) @@ -226,13 +222,6 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(bool flying READ flying NOTIFY flyingChanged) ///< Vehicle is flying Q_PROPERTY(bool landing READ landing NOTIFY landingChanged) ///< Vehicle is in landing pattern (DO_LAND_START) Q_PROPERTY(bool guidedMode READ guidedMode WRITE setGuidedMode NOTIFY guidedModeChanged) ///< Vehicle is in Guided mode and can respond to guided commands - Q_PROPERTY(bool guidedModeSupported READ guidedModeSupported CONSTANT) ///< Guided mode commands are supported by this vehicle - Q_PROPERTY(bool pauseVehicleSupported READ pauseVehicleSupported CONSTANT) ///< Pause vehicle command is supported - Q_PROPERTY(bool orbitModeSupported READ orbitModeSupported CONSTANT) ///< Orbit mode is supported by this vehicle - Q_PROPERTY(bool roiModeSupported READ roiModeSupported CONSTANT) ///< Orbit mode is supported by this vehicle - Q_PROPERTY(bool takeoffVehicleSupported READ takeoffVehicleSupported CONSTANT) ///< Takeoff supported - Q_PROPERTY(bool guidedTakeoffSupported READ guidedTakeoffSupported CONSTANT) ///< Guided takeoff supported - Q_PROPERTY(bool changeHeadingSupported READ changeHeadingSupported CONSTANT) ///< Change Heading supported Q_PROPERTY(QString gotoFlightMode READ gotoFlightMode CONSTANT) ///< Flight mode vehicle is in while performing goto Q_PROPERTY(bool haveMRSpeedLimits READ haveMRSpeedLimits NOTIFY haveMRSpeedLimChanged) Q_PROPERTY(bool haveFWSpeedLimits READ haveFWSpeedLimits NOTIFY haveFWSpeedLimChanged) @@ -411,14 +400,9 @@ class Vehicle : public VehicleFactGroup Q_INVOKABLE QVariant expandedToolbarIndicatorSource(const QString& indicatorName); bool isInitialConnectComplete() const; - bool guidedModeSupported () const; - bool pauseVehicleSupported () const; - bool orbitModeSupported () const; - bool roiModeSupported () const; - bool takeoffVehicleSupported () const; - bool guidedTakeoffSupported () const; - bool changeHeadingSupported () const; QString gotoFlightMode () const; + + VehicleSupports* supports() { return _vehicleSupports; } bool hasGripper () const; bool haveMRSpeedLimits() const { return _multirotor_speed_limits_available; } bool haveFWSpeedLimits() const { return _fixed_wing_airspeed_limits_available; } @@ -482,12 +466,7 @@ class Vehicle : public VehicleFactGroup bool sub() const; bool spacecraft() const; - bool supportsThrottleModeCenterZero () const; - bool supportsNegativeThrust (); - bool supportsRadio () const; - bool supportsJSButton () const; - bool supportsMotorInterference () const; - bool supportsTerrainFrame () const; + void setGuidedMode(bool guidedMode); @@ -537,7 +516,6 @@ class Vehicle : public VehicleFactGroup QString pauseFlightMode () const; QString rtlFlightMode () const; QString smartRTLFlightMode () const; - bool supportsSmartRTL () const; QString landFlightMode () const; QString takeControlFlightMode () const; QString followFlightMode () const; @@ -1050,6 +1028,7 @@ void _activeVehicleChanged (Vehicle* newActiveVehicle); VehicleObjectAvoidance* _objectAvoidance = nullptr; Autotune* _autotune = nullptr; GimbalController* _gimbalController = nullptr; + VehicleSupports* _vehicleSupports = nullptr; bool _armed = false; ///< true: vehicle is armed uint8_t _base_mode = 0; ///< base_mode from HEARTBEAT diff --git a/src/Vehicle/VehicleSetup/JoystickComponentButtons.qml b/src/Vehicle/VehicleSetup/JoystickComponentButtons.qml index 1a00009b14ee..0667eee23ba7 100644 --- a/src/Vehicle/VehicleSetup/JoystickComponentButtons.qml +++ b/src/Vehicle/VehicleSetup/JoystickComponentButtons.qml @@ -107,7 +107,7 @@ ColumnLayout { /*Column { id: buttonCol Layout.fillWidth: true - visible: globals.activeVehicle.supportsJSButton + visible: globals.activeVehicle.supports.jsButton spacing: ScreenTools.defaultFontPixelHeight / 3 Row { @@ -123,7 +123,7 @@ ColumnLayout { } QGCLabel { width: ScreenTools.defaultFontPixelWidth * 26 - visible: globals.activeVehicle.supportsJSButton + visible: globals.activeVehicle.supports.jsButton text: qsTr("Shift Function: ") } } @@ -133,7 +133,7 @@ ColumnLayout { Row { spacing: ScreenTools.defaultFontPixelWidth - visible: globals.activeVehicle.supportsJSButton + visible: globals.activeVehicle.supports.jsButton property var parameterName: `BTN${index}_FUNCTION` property var parameterShiftName: `BTN${index}_SFUNCTION` property bool hasFirmwareSupport: controller.parameterExists(-1, parameterName) @@ -217,7 +217,7 @@ ColumnLayout { id: repeatCheck text: qsTr("Repeat") enabled: currentAssignableAction && joystick.calibrated && currentAssignableAction.canRepeat - visible: !globals.activeVehicle.supportsJSButton + visible: !globals.activeVehicle.supports.jsButton onClicked: { joystick.setButtonRepeat(modelData, checked) diff --git a/src/Vehicle/VehicleSetup/JoystickComponentSettings.qml b/src/Vehicle/VehicleSetup/JoystickComponentSettings.qml index 269c9500a769..e7ba50d6d247 100644 --- a/src/Vehicle/VehicleSetup/JoystickComponentSettings.qml +++ b/src/Vehicle/VehicleSetup/JoystickComponentSettings.qml @@ -46,7 +46,7 @@ ColumnLayout { Layout.fillWidth: true text: qsTr("Negative Thrust") fact: _joystickSettings.negativeThrust - visible: globals.activeVehicle.supportsNegativeThrust && fact.visible + visible: globals.activeVehicle.supports.negativeThrust && fact.visible } QGCCheckBoxSlider { diff --git a/src/Vehicle/VehicleSupports.cc b/src/Vehicle/VehicleSupports.cc new file mode 100644 index 000000000000..7529ee262185 --- /dev/null +++ b/src/Vehicle/VehicleSupports.cc @@ -0,0 +1,88 @@ +#include "VehicleSupports.h" + +#include "FirmwarePlugin/FirmwarePlugin.h" +#include "Vehicle.h" + +VehicleSupports::VehicleSupports(Vehicle *vehicle) + : QObject(vehicle) + , _vehicle(vehicle) +{ + connect(_vehicle, &Vehicle::firmwareTypeChanged, this, &VehicleSupports::terrainFrameChanged); +} + +bool VehicleSupports::throttleModeCenterZero() const +{ + return _vehicle->firmwarePlugin()->supportsThrottleModeCenterZero(); +} + +bool VehicleSupports::negativeThrust() const +{ + return _vehicle->firmwarePlugin()->supportsNegativeThrust(_vehicle); +} + +bool VehicleSupports::jsButton() const +{ + return _vehicle->firmwarePlugin()->supportsJSButton(); +} + +bool VehicleSupports::radio() const +{ + return _vehicle->firmwarePlugin()->supportsRadio(); +} + +bool VehicleSupports::motorInterference() const +{ + return _vehicle->firmwarePlugin()->supportsMotorInterference(); +} + +bool VehicleSupports::smartRTL() const +{ + return _vehicle->firmwarePlugin()->supportsSmartRTL(); +} + +bool VehicleSupports::terrainFrame() const +{ + return !_vehicle->px4Firmware(); +} + +bool VehicleSupports::guidedMode() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::GuidedModeCapability); +} + +bool VehicleSupports::pauseVehicle() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::PauseVehicleCapability); +} + +bool VehicleSupports::orbitMode() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::OrbitModeCapability); +} + +bool VehicleSupports::roiMode() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::ROIModeCapability); +} + +bool VehicleSupports::takeoffMissionCommand() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::TakeoffVehicleCapability); +} + +bool VehicleSupports::guidedTakeoffWithAltitude() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::GuidedTakeoffCapability); +} + +bool VehicleSupports::guidedTakeoffWithoutAltitude() const +{ + auto* fp = _vehicle->firmwarePlugin(); + return fp->isCapable(_vehicle, FirmwarePlugin::TakeoffVehicleCapability) + && !fp->isCapable(_vehicle, FirmwarePlugin::GuidedTakeoffCapability); +} + +bool VehicleSupports::changeHeading() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::ChangeHeadingCapability); +} diff --git a/src/Vehicle/VehicleSupports.h b/src/Vehicle/VehicleSupports.h new file mode 100644 index 000000000000..dc7c45b4e687 --- /dev/null +++ b/src/Vehicle/VehicleSupports.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +class FirmwarePlugin; +class Vehicle; + +class VehicleSupports : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + +public: + explicit VehicleSupports(Vehicle *vehicle); + + Q_PROPERTY(bool throttleModeCenterZero READ throttleModeCenterZero CONSTANT) + Q_PROPERTY(bool negativeThrust READ negativeThrust CONSTANT) + Q_PROPERTY(bool jsButton READ jsButton CONSTANT) + Q_PROPERTY(bool radio READ radio CONSTANT) + Q_PROPERTY(bool motorInterference READ motorInterference CONSTANT) + Q_PROPERTY(bool smartRTL READ smartRTL CONSTANT) + Q_PROPERTY(bool terrainFrame READ terrainFrame NOTIFY terrainFrameChanged) + Q_PROPERTY(bool guidedMode READ guidedMode CONSTANT) + Q_PROPERTY(bool pauseVehicle READ pauseVehicle CONSTANT) + Q_PROPERTY(bool orbitMode READ orbitMode CONSTANT) + Q_PROPERTY(bool roiMode READ roiMode CONSTANT) + Q_PROPERTY(bool takeoffMissionCommand READ takeoffMissionCommand CONSTANT) + Q_PROPERTY(bool guidedTakeoffWithAltitude READ guidedTakeoffWithAltitude CONSTANT) + Q_PROPERTY(bool guidedTakeoffWithoutAltitude READ guidedTakeoffWithoutAltitude CONSTANT) + Q_PROPERTY(bool changeHeading READ changeHeading CONSTANT) + + bool throttleModeCenterZero() const; + bool negativeThrust() const; + bool jsButton() const; + bool radio() const; + bool motorInterference() const; + bool smartRTL() const; + bool terrainFrame() const; + bool guidedMode() const; + bool pauseVehicle() const; + bool orbitMode() const; + bool roiMode() const; + bool takeoffMissionCommand() const; + bool guidedTakeoffWithAltitude() const; + bool guidedTakeoffWithoutAltitude() const; + bool changeHeading() const; + +signals: + void terrainFrameChanged(); + +private: + Vehicle *_vehicle = nullptr; +}; From 0760cdc2f2e4b9c0366f78b2c6336d0cf2c53346 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 7 Mar 2026 13:38:50 -0800 Subject: [PATCH 048/119] Refactor PlanView TreeView: QPersistentModelIndex, independent expand/collapse, signal-based layer changes - Replace hardcoded row indices with QPersistentModelIndex properties on MissionController, resolved dynamically via rowAtIndex() in QML - Make group expand/collapse independent (not mutually exclusive) - Replace dynamic-scoped _editingLayer writes with an explicit editingLayerChangeRequested signal chain through the component tree - Add rally-point help text in PlanToolBarIndicators, controlled by a showRallyPointsHelp bool property - Hide rally header child item in TreeView when rally points already exist - Fix shutdown crash: reorder member declarations so _visualItemsTree is destroyed before its child QObjects, and skip _disconnectSubtree in QmlObjectTreeModel destructor (objects may already be destroyed) - Use implicitHeight fallback of 1 instead of 0 to suppress TreeView warning - Redesign RallyPoint.svg and adjust GeoFence.svg - Add unit tests for QPersistentModelIndex getters and QmlObjectTreeModel destructor safety --- resources/GeoFence.svg | 6 +- resources/RallyPoint.svg | 7 +- src/MissionManager/MissionController.cc | 34 +++-- src/MissionManager/MissionController.h | 12 +- src/QmlControls/MissionItemTreeView.qml | 122 ++++-------------- src/QmlControls/PlanToolBarIndicators.qml | 7 + src/QmlControls/PlanView.qml | 2 + src/QmlControls/PlanViewRightPanel.qml | 3 + src/QmlControls/PlanViewToolBar.qml | 2 + src/QmlControls/QmlObjectTreeModel.cc | 3 +- src/QmlControls/RallyPointEditorHeader.qml | 2 +- .../MissionControllerTreeTest.cc | 94 ++++++++++++++ .../MissionControllerTreeTest.h | 9 ++ test/QmlControls/QmlObjectTreeModelTest.cc | 32 +++++ test/QmlControls/QmlObjectTreeModelTest.h | 6 + 15 files changed, 229 insertions(+), 112 deletions(-) diff --git a/resources/GeoFence.svg b/resources/GeoFence.svg index dcc9c279f412..5a61004cfdaf 100644 --- a/resources/GeoFence.svg +++ b/resources/GeoFence.svg @@ -1,8 +1,8 @@ - - + + - + diff --git a/resources/RallyPoint.svg b/resources/RallyPoint.svg index 34498b3b17b2..28d2870297cd 100644 --- a/resources/RallyPoint.svg +++ b/resources/RallyPoint.svg @@ -1,3 +1,6 @@ - - + + + + + diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index b9a2eec92d09..369f52a1b4a7 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -1911,36 +1911,50 @@ void MissionController::_onRallyPointsInserted(const QModelIndex& parent, int fi auto* rallyController = _masterController->rallyPointController(); if (!rallyController) return; + // If this is the first rally point, remove the header marker + if (first == 0 && _visualItemsTree.rowCount(_rallyGroupIndex) == 1) { + const QModelIndex child = _visualItemsTree.index(0, 0, _rallyGroupIndex); + if (_visualItemsTree.data(child, QmlObjectTreeModel::NodeTypeRole).toString() == QStringLiteral("rallyHeader")) { + _visualItemsTree.removeItem(child); + } + } + auto* pts = rallyController->points(); - // Rally children: index 0 is the rallyHeader marker, rally items start at index 1 for (int i = first; i <= last; i++) { - _visualItemsTree.insertItem(i + 1, (*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem")); + _visualItemsTree.insertItem(i, (*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem")); } } void MissionController::_onRallyPointsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { Q_UNUSED(parent); - // Rally children: index 0 is header marker, rally items start at index 1 for (int i = last; i >= first; i--) { - _visualItemsTree.removeAt(_rallyGroupIndex, i + 1); + _visualItemsTree.removeAt(_rallyGroupIndex, i); + } + + // If all rally points are being removed, re-add the header marker + auto* rallyController = _masterController->rallyPointController(); + if (rallyController && (rallyController->points()->count() - (last - first + 1)) == 0) { + _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader")); + _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader")); } } void MissionController::_onRallyPointsReset(void) { - // Remove all rally children except the header marker (index 0) - while (_visualItemsTree.rowCount(_rallyGroupIndex) > 1) { - _visualItemsTree.removeAt(_rallyGroupIndex, _visualItemsTree.rowCount(_rallyGroupIndex) - 1); - } + // Remove all rally children + _visualItemsTree.removeChildren(_rallyGroupIndex); - // Repopulate + // Repopulate — either rally items or the header marker auto* rallyController = _masterController->rallyPointController(); - if (rallyController) { + if (rallyController && rallyController->points()->count() > 0) { auto* pts = rallyController->points(); for (int i = 0; i < pts->count(); i++) { _visualItemsTree.appendItem((*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem")); } + } else { + _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader")); + _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader")); } } diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index f0d2cd132da1..25dca09babcb 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -75,6 +75,11 @@ class MissionController : public PlanElementController Q_PROPERTY(QmlObjectListModel* visualItems READ visualItems NOTIFY visualItemsChanged) Q_PROPERTY(QmlObjectTreeModel* visualItemsTree READ visualItemsTree CONSTANT) ///< Tree-structured view of visualItems for TreeView + Q_PROPERTY(QPersistentModelIndex planFileGroupIndex READ planFileGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex defaultsGroupIndex READ defaultsGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex missionGroupIndex READ missionGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex fenceGroupIndex READ fenceGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex rallyGroupIndex READ rallyGroupIndex CONSTANT) Q_PROPERTY(QmlObjectListModel* simpleFlightPathSegments READ simpleFlightPathSegments CONSTANT) ///< Used by Plan view only for interactive editing Q_PROPERTY(QmlObjectListModel* directionArrows READ directionArrows CONSTANT) Q_PROPERTY(QStringList complexMissionItemNames READ complexMissionItemNames NOTIFY complexMissionItemNamesChanged) @@ -271,6 +276,11 @@ class MissionController : public PlanElementController QmlObjectListModel* visualItems (void) { return _visualItems; } QmlObjectTreeModel* visualItemsTree (void) { return &_visualItemsTree; } + QPersistentModelIndex planFileGroupIndex (void) const { return _planFileGroupIndex; } + QPersistentModelIndex defaultsGroupIndex (void) const { return _defaultsGroupIndex; } + QPersistentModelIndex missionGroupIndex (void) const { return _missionGroupIndex; } + QPersistentModelIndex fenceGroupIndex (void) const { return _fenceGroupIndex; } + QPersistentModelIndex rallyGroupIndex (void) const { return _rallyGroupIndex; } QmlObjectListModel* simpleFlightPathSegments (void) { return &_simpleFlightPathSegments; } QmlObjectListModel* directionArrows (void) { return &_directionArrows; } QStringList complexMissionItemNames (void) const; @@ -436,7 +446,6 @@ private slots: MissionManager* _missionManager = nullptr; int _missionItemCount = 0; QmlObjectListModel* _visualItems = nullptr; - QmlObjectTreeModel _visualItemsTree; QPersistentModelIndex _planFileGroupIndex; ///< Persistent index for "Plan File" group in tree QPersistentModelIndex _defaultsGroupIndex; ///< Persistent index for "Defaults" group in tree QPersistentModelIndex _missionGroupIndex; ///< Persistent index for "Mission Items" group in tree @@ -451,6 +460,7 @@ private slots: QObject _rallyGroupNode; ///< Group node for "Rally Points" in tree view QObject _fenceEditorMarker; ///< Marker child for GeoFenceEditor delegate QObject _rallyHeaderMarker; ///< Marker child for RallyPointEditorHeader delegate + QmlObjectTreeModel _visualItemsTree; // Must be declared after group nodes so it's destroyed first MissionSettingsItem* _settingsItem = nullptr; PlanViewSettings* _planViewSettings = nullptr; QmlObjectListModel _simpleFlightPathSegments; diff --git a/src/QmlControls/MissionItemTreeView.qml b/src/QmlControls/MissionItemTreeView.qml index 6597a7c634c2..17368b4443ed 100644 --- a/src/QmlControls/MissionItemTreeView.qml +++ b/src/QmlControls/MissionItemTreeView.qml @@ -14,6 +14,12 @@ TreeView { required property var editorMap required property var planMasterController + signal editingLayerChangeRequested(int layer) + + readonly property int _layerMission: 1 + readonly property int _layerFence: 2 + readonly property int _layerRally: 3 + property var _missionController: planMasterController.missionController property var _geoFenceController: planMasterController.geoFenceController property var _rallyPointController: planMasterController.rallyPointController @@ -26,12 +32,8 @@ TreeView { selectionBehavior: TableView.SelectionDisabled rowSpacing: 2 - // After collapseRecursively() groups are always at these row indices - readonly property int _rowPlanFile: 0 - readonly property int _rowDefaults: 1 - readonly property int _rowMission: 2 - readonly property int _rowFence: 3 - readonly property int _rowRally: 4 + // Helper: convert a persistent model index to the current visual row + function _rowFor(modelIndex) { return root.rowAtIndex(modelIndex) } // QGCFlickableScrollIndicator expects parent to have indicatorColor (provided by QGCFlickable/QGCListView) property color indicatorColor: qgcPal.text @@ -41,117 +43,49 @@ TreeView { QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Horizontal } QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Vertical } - Component.onCompleted: { - // Expand only Mission Items by default - root.expand(_rowMission) - } - Connections { target: root._missionController function onVisualItemsChanged() { // Mission group always expanded after rebuild (clear / load) root.collapseRecursively() - root.expand(_rowMission) - _editingLayer = _layerMission + root.expand(_rowFor(_missionController.missionGroupIndex)) + root.editingLayerChangeRequested(root._layerMission) } } - // Public API: select a layer and expand its group (no toggle — always selects). + // Public API: select a layer and expand its group. Called by the layer tool buttons. function selectLayer(nodeType) { - root.collapseRecursively() - let targetRow = -1 switch (nodeType) { case "missionGroup": - targetRow = _rowMission - _editingLayer = _layerMission + targetRow = _rowFor(_missionController.missionGroupIndex) + editingLayerChangeRequested(_layerMission) break case "fenceGroup": - targetRow = _rowFence - _editingLayer = _layerFence + targetRow = _rowFor(_missionController.fenceGroupIndex) + editingLayerChangeRequested(_layerFence) break case "rallyGroup": - targetRow = _rowRally - _editingLayer = _layerRally + targetRow = _rowFor(_missionController.rallyGroupIndex) + editingLayerChangeRequested(_layerRally) break } if (targetRow >= 0) { - root.expand(targetRow) + if (!root.isExpanded(targetRow)) + root.expand(targetRow) root.forceLayout() root.positionViewAtRow(targetRow, TableView.AlignTop) } } - // Switching editing layer on group expand — exclusive: only one group expanded at a time. - // Clicking an already-expanded group collapses it (toggle behavior). - function _expandExclusive(clickedNodeType) { - // Check if the clicked group is already expanded - let alreadyActive = false - switch (clickedNodeType) { - case "planFileGroup": - alreadyActive = root.isExpanded(_rowPlanFile) - break - case "defaultsGroup": - alreadyActive = root.isExpanded(_rowDefaults) - break - case "missionGroup": - alreadyActive = _editingLayer === _layerMission - break - case "fenceGroup": - alreadyActive = _editingLayer === _layerFence - break - case "rallyGroup": - alreadyActive = _editingLayer === _layerRally - break - default: - alreadyActive = false - break - } - - // Collapse everything - root.collapseRecursively() - - // If the group was already expanded, just leave everything collapsed - if (alreadyActive) { - _editingLayer = -1 - root.forceLayout() - root.positionViewAtRow(0, TableView.AlignTop) - return - } - - // Determine target visual row and editing layer from nodeType - let targetRow = -1 - switch (clickedNodeType) { - case "planFileGroup": - targetRow = _rowPlanFile - _editingLayer = -1 - break - case "defaultsGroup": - targetRow = _rowDefaults - _editingLayer = -1 - break - case "missionGroup": - targetRow = _rowMission - _editingLayer = _layerMission - break - case "fenceGroup": - targetRow = _rowFence - _editingLayer = _layerFence - break - case "rallyGroup": - targetRow = _rowRally - _editingLayer = _layerRally - break - } - - if (targetRow >= 0) { - root.expand(targetRow) - // After collapse/expand the view may still be scrolled past the new content. - // Force layout then scroll so the expanded group header is at the top. - root.forceLayout() - root.positionViewAtRow(targetRow, TableView.AlignTop) - } + // Toggle expand/collapse for a group header. Does not affect the editing layer. + function _toggleGroup(row) { + if (root.isExpanded(row)) + root.collapse(row) + else + root.expand(row) + root.forceLayout() } // Coalesces multiple delegate height changes into a single forceLayout() call @@ -178,7 +112,7 @@ TreeView { readonly property string nodeType: model.nodeType implicitWidth: root.width - implicitHeight: loader.item ? loader.item.height : 0 + implicitHeight: loader.item ? loader.item.height : 1 width: root.width height: implicitHeight @@ -243,7 +177,7 @@ TreeView { MouseArea { anchors.fill: parent - onClicked: root._expandExclusive(delegateRoot.nodeType) + onClicked: root._toggleGroup(delegateRoot.row) } } } diff --git a/src/QmlControls/PlanToolBarIndicators.qml b/src/QmlControls/PlanToolBarIndicators.qml index 107f4e14189c..f4955a0eceab 100644 --- a/src/QmlControls/PlanToolBarIndicators.qml +++ b/src/QmlControls/PlanToolBarIndicators.qml @@ -10,6 +10,7 @@ import QGroundControl.FactControls // Toolbar for Plan View RowLayout { required property var planMasterController + property bool showRallyPointsHelp: false id: root spacing: ScreenTools.defaultFontPixelWidth @@ -156,6 +157,12 @@ RowLayout { } } + QGCLabel { + text: qsTr("Click in map to add rally points") + visible: root.showRallyPointsHelp + Layout.alignment: Qt.AlignVCenter + } + Component { id: hamburgerDropPanelComponent diff --git a/src/QmlControls/PlanView.qml b/src/QmlControls/PlanView.qml index 9d543d56c40e..f1bbcee23251 100644 --- a/src/QmlControls/PlanView.qml +++ b/src/QmlControls/PlanView.qml @@ -217,6 +217,7 @@ Item { PlanViewToolBar { id: planToolBar planMasterController: _planMasterController + showRallyPointsHelp: _editingLayer === _layerRally } Item { @@ -499,6 +500,7 @@ Item { width: _rightPanelWidth planMasterController: _planMasterController editorMap: editorMap + onEditingLayerChangeRequested: (layer) => _editingLayer = layer } // Layer switching icons — only active icon visible; click to expand choices leftward diff --git a/src/QmlControls/PlanViewRightPanel.qml b/src/QmlControls/PlanViewRightPanel.qml index b85d33951e2d..7531dfe2f9c2 100644 --- a/src/QmlControls/PlanViewRightPanel.qml +++ b/src/QmlControls/PlanViewRightPanel.qml @@ -9,6 +9,8 @@ Item { required property var editorMap required property var planMasterController + signal editingLayerChangeRequested(int layer) + id: root property var _missionController: planMasterController.missionController @@ -93,6 +95,7 @@ Item { anchors.fill: parent editorMap: root.editorMap planMasterController: root.planMasterController + onEditingLayerChangeRequested: (layer) => root.editingLayerChangeRequested(layer) } } diff --git a/src/QmlControls/PlanViewToolBar.qml b/src/QmlControls/PlanViewToolBar.qml index 60a20f04fc06..57095c2d2f5c 100644 --- a/src/QmlControls/PlanViewToolBar.qml +++ b/src/QmlControls/PlanViewToolBar.qml @@ -13,6 +13,7 @@ Rectangle { color: qgcPal.toolbarBackground property var planMasterController + property bool showRallyPointsHelp: false property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle property real _controllerProgressPct: planMasterController.missionController.progressPct @@ -52,6 +53,7 @@ Rectangle { anchors.top: parent.top anchors.bottom: parent.bottom planMasterController: _root.planMasterController + showRallyPointsHelp: _root.showRallyPointsHelp } } diff --git a/src/QmlControls/QmlObjectTreeModel.cc b/src/QmlControls/QmlObjectTreeModel.cc index ac1015f30244..56fecb1ba10e 100644 --- a/src/QmlControls/QmlObjectTreeModel.cc +++ b/src/QmlControls/QmlObjectTreeModel.cc @@ -64,7 +64,8 @@ QmlObjectTreeModel::QmlObjectTreeModel(QObject* parent) QmlObjectTreeModel::~QmlObjectTreeModel() { - _disconnectSubtree(&_rootNode); + // Skip disconnect — objects may already be destroyed during application shutdown. + // Just delete the tree nodes. _deleteSubtree(&_rootNode, false); } diff --git a/src/QmlControls/RallyPointEditorHeader.qml b/src/QmlControls/RallyPointEditorHeader.qml index 11172512a113..70fb4c8374c7 100644 --- a/src/QmlControls/RallyPointEditorHeader.qml +++ b/src/QmlControls/RallyPointEditorHeader.qml @@ -41,7 +41,7 @@ Rectangle { anchors.right: parent.right wrapMode: Text.WordWrap font.pointSize: ScreenTools.smallFontPointSize - text: qsTr("Rally Points provide alternate landing points when performing a Return to Launch (RTL).\n\nClick on the map to add Rally Points.") + text: qsTr("Rally Points provide alternate landing points when performing a Return to Launch (RTL).") } } } diff --git a/test/MissionManager/MissionControllerTreeTest.cc b/test/MissionManager/MissionControllerTreeTest.cc index 957f1bc4fe63..5a250bb62f8f 100644 --- a/test/MissionManager/MissionControllerTreeTest.cc +++ b/test/MissionManager/MissionControllerTreeTest.cc @@ -5,6 +5,7 @@ #include "MissionSettingsItem.h" #include "PlanMasterController.h" #include "QmlObjectTreeModel.h" +#include "RallyPointController.h" #include "SettingsManager.h" #include "SimpleMissionItem.h" #include "TestFixtures.h" @@ -275,6 +276,99 @@ void MissionControllerTreeTest::_testRecalcChildItemsNoCrash() QCOMPARE(tree->rowCount(), kGroupCount); } +// =========================================================================== +// Exposed Q_PROPERTY persistent index getters match tree model indices +// =========================================================================== + +void MissionControllerTreeTest::_testExposedPersistentIndexGettersMatchTree() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + + QCOMPARE(QModelIndex(_missionController->planFileGroupIndex()), tree->index(kPlanFileGroupRow, 0)); + QCOMPARE(QModelIndex(_missionController->defaultsGroupIndex()), tree->index(kDefaultsGroupRow, 0)); + QCOMPARE(QModelIndex(_missionController->missionGroupIndex()), tree->index(kMissionGroupRow, 0)); + QCOMPARE(QModelIndex(_missionController->fenceGroupIndex()), tree->index(kFenceGroupRow, 0)); + QCOMPARE(QModelIndex(_missionController->rallyGroupIndex()), tree->index(kRallyGroupRow, 0)); +} + +// =========================================================================== +// Exposed persistent indexes survive removeAll rebuild +// =========================================================================== + +void MissionControllerTreeTest::_testExposedIndexesSurviveRemoveAll() +{ + _initForTest(); + + // Insert some waypoints then removeAll to trigger tree rebuild + const QList waypoints = Coord::waypointPath(Coord::zurich(), 3); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + _missionController->removeAll(); + + // The CONSTANT Q_PROPERTY persistent indexes must still be valid + QVERIFY(_missionController->planFileGroupIndex().isValid()); + QVERIFY(_missionController->defaultsGroupIndex().isValid()); + QVERIFY(_missionController->missionGroupIndex().isValid()); + QVERIFY(_missionController->fenceGroupIndex().isValid()); + QVERIFY(_missionController->rallyGroupIndex().isValid()); + + // They should still point to the correct rows + QCOMPARE(_missionController->planFileGroupIndex().row(), kPlanFileGroupRow); + QCOMPARE(_missionController->defaultsGroupIndex().row(), kDefaultsGroupRow); + QCOMPARE(_missionController->missionGroupIndex().row(), kMissionGroupRow); + QCOMPARE(_missionController->fenceGroupIndex().row(), kFenceGroupRow); + QCOMPARE(_missionController->rallyGroupIndex().row(), kRallyGroupRow); +} + +// =========================================================================== +// Rally header appears when no points, disappears when points added, reappears when removed +// =========================================================================== + +void MissionControllerTreeTest::_testRallyHeaderDynamicVisibility() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + const QPersistentModelIndex rallyGroup(_missionController->rallyGroupIndex()); + auto* rallyController = _masterController->rallyPointController(); + QVERIFY(rallyController); + + // Initially: rally group has 1 child — the header marker + QCOMPARE(tree->rowCount(rallyGroup), 1); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyHeader")); + + // Add a rally point → header removed, replaced by rallyItem + rallyController->addPoint(QGeoCoordinate(47.3977, 8.5456)); + QCOMPARE(tree->rowCount(rallyGroup), 1); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyItem")); + + // Add a second rally point + rallyController->addPoint(QGeoCoordinate(47.3980, 8.5460)); + QCOMPARE(tree->rowCount(rallyGroup), 2); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyItem")); + QCOMPARE(tree->data(tree->index(1, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyItem")); + + // Remove the first rally point — still 1 rallyItem, no header + rallyController->removePoint(rallyController->points()->get(0)); + QCOMPARE(tree->rowCount(rallyGroup), 1); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyItem")); + + // Remove the last rally point → header marker reappears + rallyController->removePoint(rallyController->points()->get(0)); + QCOMPARE(tree->rowCount(rallyGroup), 1); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyHeader")); +} + #include "UnitTest.h" UT_REGISTER_TEST(MissionControllerTreeTest, TestLabel::Integration, TestLabel::MissionManager) diff --git a/test/MissionManager/MissionControllerTreeTest.h b/test/MissionManager/MissionControllerTreeTest.h index 81951cc78c08..c1579a548c5d 100644 --- a/test/MissionManager/MissionControllerTreeTest.h +++ b/test/MissionManager/MissionControllerTreeTest.h @@ -37,6 +37,15 @@ private slots: // recalcChildItems is a no-op for tree (no crash) void _testRecalcChildItemsNoCrash(); + // Exposed Q_PROPERTY persistent index getters match tree model indices + void _testExposedPersistentIndexGettersMatchTree(); + + // Exposed persistent indexes survive removeAll rebuild + void _testExposedIndexesSurviveRemoveAll(); + + // Rally header appears/disappears as rally points are added/removed + void _testRallyHeaderDynamicVisibility(); + private: void _initForTest(); diff --git a/test/QmlControls/QmlObjectTreeModelTest.cc b/test/QmlControls/QmlObjectTreeModelTest.cc index 9adf2d560a55..645d23ce8c04 100644 --- a/test/QmlControls/QmlObjectTreeModelTest.cc +++ b/test/QmlControls/QmlObjectTreeModelTest.cc @@ -680,6 +680,38 @@ void QmlObjectTreeModelTest::_insertNullObject() QVERIFY(!model.data(idx, Qt::UserRole).isValid()); // null object → invalid variant } +// --------------------------------------------------------------------------- +// Destructor safety: external objects outlive the model +// --------------------------------------------------------------------------- + +void QmlObjectTreeModelTest::_destructorSafeWithExternalObjects() +{ + QObject externalObj; + { + QmlObjectTreeModel model; + model.appendItem(&externalObj, QModelIndex(), QStringLiteral("test")); + // model destroyed here — must not crash or double-free externalObj + } + // externalObj still alive — reaching here means no crash + QVERIFY(true); +} + +// --------------------------------------------------------------------------- +// Destructor safety: objects destroyed before the model (shutdown scenario) +// --------------------------------------------------------------------------- + +void QmlObjectTreeModelTest::_destructorSafeWithDestroyedObjects() +{ + QmlObjectTreeModel model; + { + QObject tempObj; + model.appendItem(&tempObj, QModelIndex(), QStringLiteral("test")); + // tempObj destroyed here while model still holds a pointer to it + } + // model destroyed after tempObj — must not crash during ~QmlObjectTreeModel + QVERIFY(true); +} + UT_REGISTER_TEST(QmlObjectTreeModelTest, TestLabel::Unit) #include "QmlObjectTreeModelTest.moc" diff --git a/test/QmlControls/QmlObjectTreeModelTest.h b/test/QmlControls/QmlObjectTreeModelTest.h index 83fcd5aa9f28..f397292355de 100644 --- a/test/QmlControls/QmlObjectTreeModelTest.h +++ b/test/QmlControls/QmlObjectTreeModelTest.h @@ -81,4 +81,10 @@ private slots: // Null object edge case void _insertNullObject(); + + // Destructor safe when external objects outlive the model + void _destructorSafeWithExternalObjects(); + + // Destructor safe when objects destroyed before the model + void _destructorSafeWithDestroyedObjects(); }; From 8d2c9fcd9326aab252438fb8fd8c180a7f582ad7 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 7 Mar 2026 17:24:35 -0800 Subject: [PATCH 049/119] Reduce compile time: Vehicle.h forward declarations, PCH optimization, earcut fix Vehicle.h: Replace 20 FactGroup/model includes with forward declarations. Convert 17 FactGroup + 2 list model members from value types to heap-allocated pointers to break the include dependency. Move 19 getter bodies out-of-line (required since forward-declared types can't implicitly convert to base class pointers). All objects are parented to the Vehicle, so ownership is unchanged. pch.h: Drop 7 low-value headers (string, vector, QDebug, QVector, 3x QtNetwork) that appeared in fewer than 11 files. Add 12 high-frequency headers including QLoggingCategory (211 files), QtQmlIntegration (127 files), QGeoCoordinate (52 files), QDir, QDateTime, QByteArray, QFileInfo, QSettings, QStringList, QThread, QElapsedTimer, QApplicationStatic, QQmlEngine, and QCoreApplication. earcut: Add EXCLUDE_FROM_ALL to CPMAddPackage to prevent the upstream fixtures OBJECT library (30+ test data .cpp files) from compiling unconditionally. Together these changes reduce clean build time by ~10%. --- src/Vehicle/TerrainProtocolHandler.cc | 1 + src/Vehicle/Vehicle.cc | 139 ++++++++++++++-------- src/Vehicle/Vehicle.h | 122 +++++++++---------- src/Vehicle/VehicleObjectAvoidance.cc | 1 + src/Viewer3D/Providers/Osm/CMakeLists.txt | 3 + src/pch.h | 32 +++-- 6 files changed, 176 insertions(+), 122 deletions(-) diff --git a/src/Vehicle/TerrainProtocolHandler.cc b/src/Vehicle/TerrainProtocolHandler.cc index 36d19d33d76d..c567178bce6c 100644 --- a/src/Vehicle/TerrainProtocolHandler.cc +++ b/src/Vehicle/TerrainProtocolHandler.cc @@ -1,4 +1,5 @@ #include "TerrainProtocolHandler.h" +#include "TerrainFactGroup.h" #include "TerrainQuery.h" #include "Vehicle.h" #include "MAVLinkProtocol.h" diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index e0a28657a14c..6038559b0919 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -1,5 +1,24 @@ #include "Vehicle.h" #include "Actuators.h" +#include "BatteryFactGroupListModel.h" +#include "EscStatusFactGroupListModel.h" +#include "TerrainFactGroup.h" +#include "VehicleClockFactGroup.h" +#include "VehicleDistanceSensorFactGroup.h" +#include "VehicleEFIFactGroup.h" +#include "VehicleEstimatorStatusFactGroup.h" +#include "VehicleGeneratorFactGroup.h" +#include "VehicleGPS2FactGroup.h" +#include "VehicleGPSFactGroup.h" +#include "VehicleGPSAggregateFactGroup.h" +#include "VehicleHygrometerFactGroup.h" +#include "VehicleLocalPositionFactGroup.h" +#include "VehicleLocalPositionSetpointFactGroup.h" +#include "VehicleRPMFactGroup.h" +#include "VehicleSetpointFactGroup.h" +#include "VehicleTemperatureFactGroup.h" +#include "VehicleVibrationFactGroup.h" +#include "VehicleWindFactGroup.h" #include "VehicleSupports.h" #include "ADSBVehicleManager.h" #include "AudioOutput.h" @@ -87,24 +106,6 @@ Vehicle::Vehicle(LinkInterface* link, , _trajectoryPoints (new TrajectoryPoints(this, this)) , _mavlinkStreamConfig (std::bind(&Vehicle::_setMessageInterval, this, std::placeholders::_1, std::placeholders::_2)) , _vehicleFactGroup (this) - , _gpsFactGroup (this) - , _gps2FactGroup (this) - , _gpsAggregateFactGroup (this) - , _windFactGroup (this) - , _vibrationFactGroup (this) - , _temperatureFactGroup (this) - , _clockFactGroup (this) - , _setpointFactGroup (this) - , _distanceSensorFactGroup (this) - , _localPositionFactGroup (this) - , _localPositionSetpointFactGroup(this) - , _estimatorStatusFactGroup (this) - , _hygrometerFactGroup (this) - , _generatorFactGroup (this) - , _efiFactGroup (this) - , _rpmFactGroup (this) - , _terrainFactGroup (this) - , _terrainProtocolHandler (new TerrainProtocolHandler(this, &_terrainFactGroup, this)) { connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &Vehicle::_activeVehicleChanged); @@ -187,15 +188,6 @@ Vehicle::Vehicle(MAV_AUTOPILOT firmwareType, , _trajectoryPoints (new TrajectoryPoints(this, this)) , _mavlinkStreamConfig (std::bind(&Vehicle::_setMessageInterval, this, std::placeholders::_1, std::placeholders::_2)) , _vehicleFactGroup (this) - , _gpsFactGroup (this) - , _gps2FactGroup (this) - , _gpsAggregateFactGroup (this) - , _windFactGroup (this) - , _vibrationFactGroup (this) - , _clockFactGroup (this) - , _distanceSensorFactGroup (this) - , _localPositionFactGroup (this) - , _localPositionSetpointFactGroup (this) { // This will also set the settings based firmware/vehicle types. So it needs to happen first. if (_firmwareType == MAV_AUTOPILOT_TRACK) { @@ -299,30 +291,54 @@ void Vehicle::_commonInit(LinkInterface* link) // Flight modes can differ based on advanced mode connect(QGCCorePlugin::instance(), &QGCCorePlugin::showAdvancedUIChanged, this, &Vehicle::flightModesChanged); - _gpsAggregateFactGroup.bindToGps(&_gpsFactGroup, &_gps2FactGroup); + _gpsFactGroup = new VehicleGPSFactGroup(this); + _gps2FactGroup = new VehicleGPS2FactGroup(this); + _gpsAggregateFactGroup = new VehicleGPSAggregateFactGroup(this); + _windFactGroup = new VehicleWindFactGroup(this); + _vibrationFactGroup = new VehicleVibrationFactGroup(this); + _temperatureFactGroup = new VehicleTemperatureFactGroup(this); + _clockFactGroup = new VehicleClockFactGroup(this); + _setpointFactGroup = new VehicleSetpointFactGroup(this); + _distanceSensorFactGroup = new VehicleDistanceSensorFactGroup(this); + _localPositionFactGroup = new VehicleLocalPositionFactGroup(this); + _localPositionSetpointFactGroup = new VehicleLocalPositionSetpointFactGroup(this); + _estimatorStatusFactGroup = new VehicleEstimatorStatusFactGroup(this); + _hygrometerFactGroup = new VehicleHygrometerFactGroup(this); + _generatorFactGroup = new VehicleGeneratorFactGroup(this); + _efiFactGroup = new VehicleEFIFactGroup(this); + _rpmFactGroup = new VehicleRPMFactGroup(this); + _terrainFactGroup = new TerrainFactGroup(this); + _batteryFactGroupListModel = new BatteryFactGroupListModel(this); + _escStatusFactGroupListModel = new EscStatusFactGroupListModel(this); + + if (!_offlineEditingVehicle) { + _terrainProtocolHandler = new TerrainProtocolHandler(this, _terrainFactGroup, this); + } + + _gpsAggregateFactGroup->bindToGps(_gpsFactGroup, _gps2FactGroup); _createImageProtocolManager(); _createStatusTextHandler(); _createMAVLinkLogManager(); // _addFactGroup(_vehicleFactGroup, _vehicleFactGroupName); - _addFactGroup(&_gpsFactGroup, _gpsFactGroupName); - _addFactGroup(&_gps2FactGroup, _gps2FactGroupName); - _addFactGroup(&_gpsAggregateFactGroup, _gpsAggregateFactGroupName); - _addFactGroup(&_windFactGroup, _windFactGroupName); - _addFactGroup(&_vibrationFactGroup, _vibrationFactGroupName); - _addFactGroup(&_temperatureFactGroup, _temperatureFactGroupName); - _addFactGroup(&_clockFactGroup, _clockFactGroupName); - _addFactGroup(&_setpointFactGroup, _setpointFactGroupName); - _addFactGroup(&_distanceSensorFactGroup, _distanceSensorFactGroupName); - _addFactGroup(&_localPositionFactGroup, _localPositionFactGroupName); - _addFactGroup(&_localPositionSetpointFactGroup,_localPositionSetpointFactGroupName); - _addFactGroup(&_estimatorStatusFactGroup, _estimatorStatusFactGroupName); - _addFactGroup(&_hygrometerFactGroup, _hygrometerFactGroupName); - _addFactGroup(&_generatorFactGroup, _generatorFactGroupName); - _addFactGroup(&_efiFactGroup, _efiFactGroupName); - _addFactGroup(&_rpmFactGroup, _rpmFactGroupName); - _addFactGroup(&_terrainFactGroup, _terrainFactGroupName); + _addFactGroup(_gpsFactGroup, _gpsFactGroupName); + _addFactGroup(_gps2FactGroup, _gps2FactGroupName); + _addFactGroup(_gpsAggregateFactGroup, _gpsAggregateFactGroupName); + _addFactGroup(_windFactGroup, _windFactGroupName); + _addFactGroup(_vibrationFactGroup, _vibrationFactGroupName); + _addFactGroup(_temperatureFactGroup, _temperatureFactGroupName); + _addFactGroup(_clockFactGroup, _clockFactGroupName); + _addFactGroup(_setpointFactGroup, _setpointFactGroupName); + _addFactGroup(_distanceSensorFactGroup, _distanceSensorFactGroupName); + _addFactGroup(_localPositionFactGroup, _localPositionFactGroupName); + _addFactGroup(_localPositionSetpointFactGroup,_localPositionSetpointFactGroupName); + _addFactGroup(_estimatorStatusFactGroup, _estimatorStatusFactGroupName); + _addFactGroup(_hygrometerFactGroup, _hygrometerFactGroupName); + _addFactGroup(_generatorFactGroup, _generatorFactGroupName); + _addFactGroup(_efiFactGroup, _efiFactGroupName); + _addFactGroup(_rpmFactGroup, _rpmFactGroupName); + _addFactGroup(_terrainFactGroup, _terrainFactGroupName); // Add firmware-specific fact groups, if provided QMap* fwFactGroups = _firmwarePlugin->factGroups(); @@ -370,6 +386,27 @@ Vehicle::~Vehicle() _autopilotPlugin = nullptr; } +FactGroup* Vehicle::gpsFactGroup() { return _gpsFactGroup; } +FactGroup* Vehicle::gps2FactGroup() { return _gps2FactGroup; } +FactGroup* Vehicle::gpsAggregateFactGroup() { return _gpsAggregateFactGroup; } +FactGroup* Vehicle::windFactGroup() { return _windFactGroup; } +FactGroup* Vehicle::vibrationFactGroup() { return _vibrationFactGroup; } +FactGroup* Vehicle::temperatureFactGroup() { return _temperatureFactGroup; } +FactGroup* Vehicle::clockFactGroup() { return _clockFactGroup; } +FactGroup* Vehicle::setpointFactGroup() { return _setpointFactGroup; } +FactGroup* Vehicle::distanceSensorFactGroup() { return _distanceSensorFactGroup; } +FactGroup* Vehicle::localPositionFactGroup() { return _localPositionFactGroup; } +FactGroup* Vehicle::localPositionSetpointFactGroup() { return _localPositionSetpointFactGroup; } +FactGroup* Vehicle::estimatorStatusFactGroup() { return _estimatorStatusFactGroup; } +FactGroup* Vehicle::terrainFactGroup() { return _terrainFactGroup; } +FactGroup* Vehicle::hygrometerFactGroup() { return _hygrometerFactGroup; } +FactGroup* Vehicle::generatorFactGroup() { return _generatorFactGroup; } +FactGroup* Vehicle::efiFactGroup() { return _efiFactGroup; } +FactGroup* Vehicle::rpmFactGroup() { return _rpmFactGroup; } + +QmlObjectListModel* Vehicle::batteries() { return _batteryFactGroupListModel; } +QmlObjectListModel* Vehicle::escs() { return _escStatusFactGroupListModel; } + void Vehicle::_deleteCameraManager() { if(_cameraManager) { @@ -521,8 +558,8 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes _waitForMavlinkMessageMessageReceivedHandler(message); // Handle creation of dynamic fact group lists - _batteryFactGroupListModel.handleMessageForFactGroupCreation(this, message); - _escStatusFactGroupListModel.handleMessageForFactGroupCreation(this, message); + _batteryFactGroupListModel->handleMessageForFactGroupCreation(this, message); + _escStatusFactGroupListModel->handleMessageForFactGroupCreation(this, message); // Let the fact groups take a whack at the mavlink traffic for (FactGroup* factGroup : factGroups()) { @@ -1066,7 +1103,7 @@ void Vehicle::_handleBatteryStatus(mavlink_message_t& message) if (!batteryMessage.isEmpty()) { QString batteryIdStr("%1"); - if (_batteryFactGroupListModel.count() > 1) { + if (_batteryFactGroupListModel->count() > 1) { batteryIdStr = batteryIdStr.arg(batteryStatus.id); } else { batteryIdStr = batteryIdStr.arg(""); @@ -3657,9 +3694,9 @@ void Vehicle::setPIDTuningTelemetryMode(PIDTuningTelemetryMode mode) { bool liveUpdate = mode != ModeDisabled; setLiveUpdates(liveUpdate); - _setpointFactGroup.setLiveUpdates(liveUpdate); - _localPositionFactGroup.setLiveUpdates(liveUpdate); - _localPositionSetpointFactGroup.setLiveUpdates(liveUpdate); + _setpointFactGroup->setLiveUpdates(liveUpdate); + _localPositionFactGroup->setLiveUpdates(liveUpdate); + _localPositionSetpointFactGroup->setLiveUpdates(liveUpdate); switch (mode) { case ModeDisabled: diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index 404fc704c929..53641d87f970 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -17,32 +17,31 @@ #include "QGCMAVLink.h" #include "QmlObjectListModel.h" #include "SysStatusSensorInfo.h" -#include "VehicleLinkManager.h" - -#include "TerrainFactGroup.h" #include "VehicleFactGroup.h" -#include "VehicleClockFactGroup.h" -#include "VehicleDistanceSensorFactGroup.h" -#include "VehicleEFIFactGroup.h" -#include "VehicleEstimatorStatusFactGroup.h" -#include "VehicleGeneratorFactGroup.h" -#include "VehicleGPS2FactGroup.h" -#include "VehicleGPSFactGroup.h" -#include "VehicleGPSAggregateFactGroup.h" -#include "VehicleHygrometerFactGroup.h" -#include "VehicleLocalPositionFactGroup.h" -#include "VehicleLocalPositionSetpointFactGroup.h" -#include "VehicleRPMFactGroup.h" -#include "VehicleSetpointFactGroup.h" -#include "VehicleTemperatureFactGroup.h" -#include "VehicleVibrationFactGroup.h" -#include "VehicleWindFactGroup.h" -#include "GimbalController.h" -#include "BatteryFactGroupListModel.h" -#include "EscStatusFactGroupListModel.h" +#include "VehicleLinkManager.h" class Actuators; class AutoPilotPlugin; +class BatteryFactGroupListModel; +class EscStatusFactGroupListModel; +class GimbalController; +class TerrainFactGroup; +class VehicleClockFactGroup; +class VehicleDistanceSensorFactGroup; +class VehicleEFIFactGroup; +class VehicleEstimatorStatusFactGroup; +class VehicleGeneratorFactGroup; +class VehicleGPS2FactGroup; +class VehicleGPSFactGroup; +class VehicleGPSAggregateFactGroup; +class VehicleHygrometerFactGroup; +class VehicleLocalPositionFactGroup; +class VehicleLocalPositionSetpointFactGroup; +class VehicleRPMFactGroup; +class VehicleSetpointFactGroup; +class VehicleTemperatureFactGroup; +class VehicleVibrationFactGroup; +class VehicleWindFactGroup; class Autotune; class ComponentInformationManager; class EventHandler; @@ -93,6 +92,7 @@ class Vehicle : public VehicleFactGroup Q_MOC_INCLUDE("MAVLinkLogManager.h") Q_MOC_INCLUDE("LinkInterface.h") Q_MOC_INCLUDE("VehicleSupports.h") + Q_MOC_INCLUDE("GimbalController.h") friend class InitialConnectStateMachine; friend class VehicleLinkManager; @@ -551,26 +551,26 @@ class Vehicle : public VehicleFactGroup void stopUAVCANBusConfig(void); FactGroup* vehicleFactGroup () { return _vehicleFactGroup; } - FactGroup* gpsFactGroup () { return &_gpsFactGroup; } - FactGroup* gps2FactGroup () { return &_gps2FactGroup; } - FactGroup* gpsAggregateFactGroup () { return &_gpsAggregateFactGroup; } - FactGroup* windFactGroup () { return &_windFactGroup; } - FactGroup* vibrationFactGroup () { return &_vibrationFactGroup; } - FactGroup* temperatureFactGroup () { return &_temperatureFactGroup; } - FactGroup* clockFactGroup () { return &_clockFactGroup; } - FactGroup* setpointFactGroup () { return &_setpointFactGroup; } - FactGroup* distanceSensorFactGroup () { return &_distanceSensorFactGroup; } - FactGroup* localPositionFactGroup () { return &_localPositionFactGroup; } - FactGroup* localPositionSetpointFactGroup() { return &_localPositionSetpointFactGroup; } - FactGroup* estimatorStatusFactGroup () { return &_estimatorStatusFactGroup; } - FactGroup* terrainFactGroup () { return &_terrainFactGroup; } - FactGroup* hygrometerFactGroup () { return &_hygrometerFactGroup; } - FactGroup* generatorFactGroup () { return &_generatorFactGroup; } - FactGroup* efiFactGroup () { return &_efiFactGroup; } - FactGroup* rpmFactGroup () { return &_rpmFactGroup; } - - QmlObjectListModel* batteries () { return &_batteryFactGroupListModel; } - QmlObjectListModel* escs () { return &_escStatusFactGroupListModel; } + FactGroup* gpsFactGroup (); + FactGroup* gps2FactGroup (); + FactGroup* gpsAggregateFactGroup (); + FactGroup* windFactGroup (); + FactGroup* vibrationFactGroup (); + FactGroup* temperatureFactGroup (); + FactGroup* clockFactGroup (); + FactGroup* setpointFactGroup (); + FactGroup* distanceSensorFactGroup (); + FactGroup* localPositionFactGroup (); + FactGroup* localPositionSetpointFactGroup(); + FactGroup* estimatorStatusFactGroup (); + FactGroup* terrainFactGroup (); + FactGroup* hygrometerFactGroup (); + FactGroup* generatorFactGroup (); + FactGroup* efiFactGroup (); + FactGroup* rpmFactGroup (); + + QmlObjectListModel* batteries (); + QmlObjectListModel* escs (); MissionManager* missionManager () { return _missionManager; } GeoFenceManager* geoFenceManager () { return _geoFenceManager; } @@ -1218,27 +1218,27 @@ void _activeVehicleChanged (Vehicle* newActiveVehicle); const QString _rpmFactGroupName = QStringLiteral("rpm"); VehicleFactGroup* _vehicleFactGroup; - VehicleGPSFactGroup _gpsFactGroup; - VehicleGPS2FactGroup _gps2FactGroup; - VehicleGPSAggregateFactGroup _gpsAggregateFactGroup; - VehicleWindFactGroup _windFactGroup; - VehicleVibrationFactGroup _vibrationFactGroup; - VehicleTemperatureFactGroup _temperatureFactGroup; - VehicleClockFactGroup _clockFactGroup; - VehicleSetpointFactGroup _setpointFactGroup; - VehicleDistanceSensorFactGroup _distanceSensorFactGroup; - VehicleLocalPositionFactGroup _localPositionFactGroup; - VehicleLocalPositionSetpointFactGroup _localPositionSetpointFactGroup; - VehicleEstimatorStatusFactGroup _estimatorStatusFactGroup; - VehicleHygrometerFactGroup _hygrometerFactGroup; - VehicleGeneratorFactGroup _generatorFactGroup; - VehicleEFIFactGroup _efiFactGroup; - VehicleRPMFactGroup _rpmFactGroup; - TerrainFactGroup _terrainFactGroup; + VehicleGPSFactGroup* _gpsFactGroup = nullptr; + VehicleGPS2FactGroup* _gps2FactGroup = nullptr; + VehicleGPSAggregateFactGroup* _gpsAggregateFactGroup = nullptr; + VehicleWindFactGroup* _windFactGroup = nullptr; + VehicleVibrationFactGroup* _vibrationFactGroup = nullptr; + VehicleTemperatureFactGroup* _temperatureFactGroup = nullptr; + VehicleClockFactGroup* _clockFactGroup = nullptr; + VehicleSetpointFactGroup* _setpointFactGroup = nullptr; + VehicleDistanceSensorFactGroup* _distanceSensorFactGroup = nullptr; + VehicleLocalPositionFactGroup* _localPositionFactGroup = nullptr; + VehicleLocalPositionSetpointFactGroup* _localPositionSetpointFactGroup = nullptr; + VehicleEstimatorStatusFactGroup* _estimatorStatusFactGroup = nullptr; + VehicleHygrometerFactGroup* _hygrometerFactGroup = nullptr; + VehicleGeneratorFactGroup* _generatorFactGroup = nullptr; + VehicleEFIFactGroup* _efiFactGroup = nullptr; + VehicleRPMFactGroup* _rpmFactGroup = nullptr; + TerrainFactGroup* _terrainFactGroup = nullptr; // Dynamic FactGroups - BatteryFactGroupListModel _batteryFactGroupListModel; - EscStatusFactGroupListModel _escStatusFactGroupListModel; + BatteryFactGroupListModel* _batteryFactGroupListModel = nullptr; + EscStatusFactGroupListModel* _escStatusFactGroupListModel = nullptr; TerrainProtocolHandler* _terrainProtocolHandler = nullptr; diff --git a/src/Vehicle/VehicleObjectAvoidance.cc b/src/Vehicle/VehicleObjectAvoidance.cc index d903799c1abd..28fce3ac5d14 100644 --- a/src/Vehicle/VehicleObjectAvoidance.cc +++ b/src/Vehicle/VehicleObjectAvoidance.cc @@ -1,6 +1,7 @@ #include "VehicleObjectAvoidance.h" #include "Vehicle.h" #include "ParameterManager.h" +#include "VehicleSetpointFactGroup.h" //----------------------------------------------------------------------------- VehicleObjectAvoidance::VehicleObjectAvoidance(Vehicle *vehicle, QObject* parent) diff --git a/src/Viewer3D/Providers/Osm/CMakeLists.txt b/src/Viewer3D/Providers/Osm/CMakeLists.txt index 87f2b331ada2..e075946ead09 100644 --- a/src/Viewer3D/Providers/Osm/CMakeLists.txt +++ b/src/Viewer3D/Providers/Osm/CMakeLists.txt @@ -16,12 +16,15 @@ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_ # ---------------------------------------------------------------------------- # earcut.hpp — polygon triangulation +# EXCLUDE_FROM_ALL prevents earcut's unconditionally-built "fixtures" object +# library (30+ test data files) from compiling as part of our build. # ---------------------------------------------------------------------------- CPMAddPackage( NAME earcut_hpp GITHUB_REPOSITORY mapbox/earcut.hpp GIT_TAG f36ced7e50254738c4e5af1a239f5fb7b1094007 + EXCLUDE_FROM_ALL YES OPTIONS "EARCUT_BUILD_TESTS OFF" "EARCUT_BUILD_BENCH OFF" diff --git a/src/pch.h b/src/pch.h index 476c7d23b618..ecea7c1baf28 100644 --- a/src/pch.h +++ b/src/pch.h @@ -1,34 +1,46 @@ #pragma once -// STL - frequently used across codebase +// STL #include #include -#include #include -// Qt Core - used in nearly every file +// Qt Core - fundamentals +#include +#include #include +#include +#include #include +#include #include +#include #include #include +#include #include +#include #include +#include #include #include -#include +// Qt Core - JSON #include #include #include -// Qt Network - used in 30+ files -#include -#include -#include +// Qt Core - utilities +#include +#include -// Qt Quick - QQuickItem used in 30+ files +// Qt Positioning - used in 50+ files +#include + +// Qt Qml/Quick - QML integration macros used in ~130 headers +#include +#include #include // MAVLink - used in 400+ locations -#include "MAVLinkLib.h" \ No newline at end of file +#include "MAVLinkLib.h" From 9f93a115fbdd097b4c773c7a9ff2d549a657d7f6 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 7 Mar 2026 20:29:43 -0800 Subject: [PATCH 050/119] Plan View: move mission transforms to inline tree editor - Add Transform group and inline TransformEditor in MissionItemTreeView\n- Replace popup transform dialogs in MissionItemEditor\n- Rename EditPositionDialogController to TransformPositionController\n- Add Fact-backed transform fields and merged metadata JSON\n- Allow offsetMission without requiring valid home\n- Keep reposition/rotate guarded by home validity\n- Add active vehicle null-check in setFromVehicle --- src/MissionManager/MissionController.cc | 41 ++- src/MissionManager/MissionController.h | 12 +- src/QmlControls/CMakeLists.txt | 5 +- src/QmlControls/EditPositionDialog.qml | 2 +- .../EditPositionDialogController.h | 69 ---- src/QmlControls/MissionItemEditor.qml | 276 +------------- src/QmlControls/MissionItemTreeView.qml | 12 + src/QmlControls/TransformEditor.qml | 336 ++++++++++++++++++ ...sformPositionController.FactMetaData.json} | 38 +- ...ller.cc => TransformPositionController.cc} | 50 ++- src/QmlControls/TransformPositionController.h | 85 +++++ 11 files changed, 547 insertions(+), 379 deletions(-) delete mode 100644 src/QmlControls/EditPositionDialogController.h create mode 100644 src/QmlControls/TransformEditor.qml rename src/QmlControls/{EditPositionDialog.FactMetaData.json => TransformPositionController.FactMetaData.json} (56%) rename src/QmlControls/{EditPositionDialogController.cc => TransformPositionController.cc} (56%) create mode 100644 src/QmlControls/TransformPositionController.h diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 369f52a1b4a7..8eac65e289a1 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -1860,6 +1860,15 @@ void MissionController::_setupTreeModel(void) // Marker child for the rally header / instructions _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader")); _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader")); + + // ── Transform group ── + _transformGroupNode.setObjectName(tr("Transform")); + _transformGroupIndex = QPersistentModelIndex( + _visualItemsTree.appendItem(&_transformGroupNode, QModelIndex(), QStringLiteral("transformGroup"))); + + // Single marker child — delegate loads the inline TransformEditor + _transformEditorMarker.setObjectName(QStringLiteral("transformEditor")); + _visualItemsTree.appendItem(&_transformEditorMarker, _transformGroupIndex, QStringLiteral("transformEditor")); } //----------------------------------------------------------------------------- @@ -2742,22 +2751,34 @@ void MissionController::offsetMission(double eastMeters, bool offsetTakeoffItems, bool offsetLandingItems) { - if (!_settingsItem || !_settingsItem->coordinate().isValid()) { - qCWarning(MissionControllerLog) << "Cannot offset mission while home is invalid"; - return; - } - if (!qFuzzyIsNull(eastMeters) || !qFuzzyIsNull(northMeters)) { - double distanceMeters = qSqrt(eastMeters * eastMeters + northMeters * northMeters); + const double distanceMeters = qSqrt(eastMeters * eastMeters + northMeters * northMeters); double azimuthDegrees = 0.0; if (!qFuzzyIsNull(distanceMeters)) { const double azimuthRadians = qAtan2(eastMeters, northMeters); azimuthDegrees = qRadiansToDegrees(azimuthRadians); } - const QGeoCoordinate oldHome = _settingsItem->coordinate(); - QGeoCoordinate newHome = oldHome.atDistanceAndAzimuth(distanceMeters, azimuthDegrees); - repositionMission(newHome, offsetTakeoffItems, offsetLandingItems); + for (int i = 0; i < _visualItems->count(); ++i) { + auto* item = qobject_cast(_visualItems->get(i)); + if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) { + continue; + } + + if ((!offsetTakeoffItems && item->isTakeoffItem()) || + (!offsetLandingItems && item->isLandCommand())) { + continue; + } + + const QGeoCoordinate oldCoord = item->coordinate(); + if (!oldCoord.isValid()) { + continue; + } + + item->setCoordinate(oldCoord.atDistanceAndAzimuth(distanceMeters, azimuthDegrees)); + } + + setDirty(true); } if (!qFuzzyIsNull(upMeters)) { @@ -2780,6 +2801,8 @@ void MissionController::offsetMission(double eastMeters, item->applyNewAltitude(item->editableAlt() + upMeters); } } + + setDirty(true); } } diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 25dca09babcb..bfabc7f55488 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -80,6 +80,7 @@ class MissionController : public PlanElementController Q_PROPERTY(QPersistentModelIndex missionGroupIndex READ missionGroupIndex CONSTANT) Q_PROPERTY(QPersistentModelIndex fenceGroupIndex READ fenceGroupIndex CONSTANT) Q_PROPERTY(QPersistentModelIndex rallyGroupIndex READ rallyGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex transformGroupIndex READ transformGroupIndex CONSTANT) Q_PROPERTY(QmlObjectListModel* simpleFlightPathSegments READ simpleFlightPathSegments CONSTANT) ///< Used by Plan view only for interactive editing Q_PROPERTY(QmlObjectListModel* directionArrows READ directionArrows CONSTANT) Q_PROPERTY(QStringList complexMissionItemNames READ complexMissionItemNames NOTIFY complexMissionItemNamesChanged) @@ -201,9 +202,7 @@ class MissionController : public PlanElementController bool repositionLandingItems = true); /// Offsets all mission items which specify a coordinate by the specified - /// ENU amounts in meters. Home altitude remains unchanged. Requires a valid - /// planned home position; otherwise the mission is not modified and a - /// warning is logged. + /// ENU amounts in meters. Home altitude remains unchanged. /// @param eastMeters Distance to offset items to the east, in meters /// @param northMeters Distance to offset items to the north, in meters /// @param upMeters Distance to offset items upwards, in meters @@ -281,6 +280,7 @@ class MissionController : public PlanElementController QPersistentModelIndex missionGroupIndex (void) const { return _missionGroupIndex; } QPersistentModelIndex fenceGroupIndex (void) const { return _fenceGroupIndex; } QPersistentModelIndex rallyGroupIndex (void) const { return _rallyGroupIndex; } + QPersistentModelIndex transformGroupIndex (void) const { return _transformGroupIndex; } QmlObjectListModel* simpleFlightPathSegments (void) { return &_simpleFlightPathSegments; } QmlObjectListModel* directionArrows (void) { return &_directionArrows; } QStringList complexMissionItemNames (void) const; @@ -327,7 +327,8 @@ class MissionController : public PlanElementController static constexpr int kMissionGroupRow = 2; static constexpr int kFenceGroupRow = 3; static constexpr int kRallyGroupRow = 4; - static constexpr int kGroupCount = 5; + static constexpr int kTransformGroupRow = 5; + static constexpr int kGroupCount = 6; signals: void visualItemsChanged (void); @@ -451,6 +452,7 @@ private slots: QPersistentModelIndex _missionGroupIndex; ///< Persistent index for "Mission Items" group in tree QPersistentModelIndex _fenceGroupIndex; ///< Persistent index for "GeoFence" group in tree QPersistentModelIndex _rallyGroupIndex; ///< Persistent index for "Rally Points" group in tree + QPersistentModelIndex _transformGroupIndex; ///< Persistent index for "Transform" group in tree QObject _planFileGroupNode; ///< Group node for "Plan File" in tree view QObject _planFileInfoMarker; ///< Marker child for plan file info delegate QObject _defaultsGroupNode; ///< Group node for "Defaults" in tree view @@ -458,8 +460,10 @@ private slots: QObject _missionItemsGroupNode; ///< Group node for "Mission Items" in tree view QObject _fenceGroupNode; ///< Group node for "GeoFence" in tree view QObject _rallyGroupNode; ///< Group node for "Rally Points" in tree view + QObject _transformGroupNode; ///< Group node for "Transform" in tree view QObject _fenceEditorMarker; ///< Marker child for GeoFenceEditor delegate QObject _rallyHeaderMarker; ///< Marker child for RallyPointEditorHeader delegate + QObject _transformEditorMarker; ///< Marker child for TransformEditor delegate QmlObjectTreeModel _visualItemsTree; // Must be declared after group nodes so it's destroyed first MissionSettingsItem* _settingsItem = nullptr; PlanViewSettings* _planViewSettings = nullptr; diff --git a/src/QmlControls/CMakeLists.txt b/src/QmlControls/CMakeLists.txt index 3f1875bdd474..9d5dde97e8e9 100644 --- a/src/QmlControls/CMakeLists.txt +++ b/src/QmlControls/CMakeLists.txt @@ -5,8 +5,6 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE - EditPositionDialogController.cc - EditPositionDialogController.h FactValueGrid.cc FactValueGrid.h FlightPathSegment.cc @@ -66,6 +64,8 @@ target_sources(${CMAKE_PROJECT_NAME} ToolStripAction.h ToolStripActionList.cc ToolStripActionList.h + TransformPositionController.cc + TransformPositionController.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -178,6 +178,7 @@ qt_add_qml_module(QGroundControlControlsModule ToolStrip.qml ToolStripDropPanel.qml ToolStripHoverButton.qml + TransformEditor.qml ValueSlider.qml VehicleRotationCal.qml VehicleSummaryRow.qml diff --git a/src/QmlControls/EditPositionDialog.qml b/src/QmlControls/EditPositionDialog.qml index 063740d5ac21..2cf7cdf31d3f 100644 --- a/src/QmlControls/EditPositionDialog.qml +++ b/src/QmlControls/EditPositionDialog.qml @@ -22,7 +22,7 @@ QGCPopupDialog { property bool _showMGRS: coordinateSystemCombo.comboBox.currentIndex === 2 property bool _showVehicle: coordinateSystemCombo.comboBox.currentIndex === 3 - EditPositionDialogController { + TransformPositionController { id: controller Component.onCompleted: initValues() diff --git a/src/QmlControls/EditPositionDialogController.h b/src/QmlControls/EditPositionDialogController.h deleted file mode 100644 index ef56cac76468..000000000000 --- a/src/QmlControls/EditPositionDialogController.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "Fact.h" - -Q_DECLARE_LOGGING_CATEGORY(EditPositionDialogControllerLog) - -class EditPositionDialogController : public QObject -{ - Q_OBJECT - QML_ELEMENT - Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged REQUIRED) - Q_PROPERTY(Fact *latitude READ latitude CONSTANT) - Q_PROPERTY(Fact *longitude READ longitude CONSTANT) - Q_PROPERTY(Fact *zone READ zone CONSTANT) - Q_PROPERTY(Fact *hemisphere READ hemisphere CONSTANT) - Q_PROPERTY(Fact *easting READ easting CONSTANT) - Q_PROPERTY(Fact *northing READ northing CONSTANT) - Q_PROPERTY(Fact *mgrs READ mgrs CONSTANT) - -public: - explicit EditPositionDialogController(QObject *parent = nullptr); - ~EditPositionDialogController(); - - Q_INVOKABLE void initValues(); - Q_INVOKABLE void setFromGeo(); - Q_INVOKABLE void setFromUTM(); - Q_INVOKABLE void setFromMGRS(); - Q_INVOKABLE void setFromVehicle(); - - void setCoordinate(QGeoCoordinate coordinate); - QGeoCoordinate coordinate() const { return _coordinate; } - - Fact *latitude() { return _latitudeFact; } - Fact *longitude() { return _longitudeFact; } - Fact *zone() { return _zoneFact; } - Fact *hemisphere() { return _hemisphereFact; } - Fact *easting() { return _eastingFact; } - Fact *northing() { return _northingFact; } - Fact *mgrs() { return _mgrsFact; } - -signals: - void coordinateChanged(QGeoCoordinate coordinate); - -private: - QGeoCoordinate _coordinate; - - Fact *_latitudeFact = nullptr; - Fact *_longitudeFact = nullptr; - Fact *_zoneFact = nullptr; - Fact *_hemisphereFact = nullptr; - Fact *_eastingFact = nullptr; - Fact *_northingFact = nullptr; - Fact *_mgrsFact = nullptr; - - static QMap _metaDataMap; - - static constexpr const char *_latitudeFactName = "Latitude"; - static constexpr const char *_longitudeFactName = "Longitude"; - static constexpr const char *_zoneFactName = "Zone"; - static constexpr const char *_hemisphereFactName = "Hemisphere"; - static constexpr const char *_eastingFactName = "Easting"; - static constexpr const char *_northingFactName = "Northing"; - static constexpr const char *_mgrsFactName = "MGRS"; -}; diff --git a/src/QmlControls/MissionItemEditor.qml b/src/QmlControls/MissionItemEditor.qml index 97b7b168e5b3..f50079acaed0 100644 --- a/src/QmlControls/MissionItemEditor.qml +++ b/src/QmlControls/MissionItemEditor.qml @@ -272,275 +272,6 @@ Rectangle { } } - Component { - id: missionStartHamburgerMenuDropPanelComponent - - DropPanel { - id: missionStartHamburgerMenuDropPanel - onClosed: destroy() - - sourceComponent: Component { - ColumnLayout { - spacing: ScreenTools.defaultFontPixelHeight / 2 - - QGCButton { - Layout.fillWidth: true - text: qsTr("Offset mission...") - enabled: _missionController.plannedHomePosition.isValid - onClicked: { - offsetMissionDialogComponent.createObject(mainWindow).open() - missionStartHamburgerMenuDropPanel.close() - } - } - - QGCButton { - Layout.fillWidth: true - text: qsTr("Rotate mission...") - enabled: _missionController.plannedHomePosition.isValid - onClicked: { - rotateMissionDialogComponent.createObject(mainWindow).open() - missionStartHamburgerMenuDropPanel.close() - } - } - - QGCButton { - Layout.fillWidth: true - text: qsTr("Edit mission position...") - enabled: _missionController.plannedHomePosition.isValid - onClicked: { - editMissionPositionDialogComponent.createObject(mainWindow).open() - missionStartHamburgerMenuDropPanel.close() - } - } - } - } - } - } - - Component { - id: offsetMissionDialogComponent - - QGCPopupDialog { - id: root - title: qsTr("Offset Mission") - buttons: Dialog.Cancel | Dialog.Ok - modal: true - - property real _margin: ScreenTools.defaultFontPixelWidth / 2 - property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 20 - property real _labelWidth: ScreenTools.defaultFontPixelWidth * 14 - - ColumnLayout { - spacing: _margin - - RowLayout { - spacing: _margin - Layout.fillWidth: true - - QGCLabel { - text: qsTr("East (m)") - Layout.preferredWidth: _labelWidth - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - } - - QGCTextField { - id: eastField - text: "0" - Layout.preferredWidth: _textFieldWidth - Layout.fillWidth: true - inputMethodHints: Qt.ImhPreferNumbers - validator: DoubleValidator { - notation: DoubleValidator.StandardNotation - locale: Qt.locale() - } - } - } - - RowLayout { - spacing: _margin - Layout.fillWidth: true - - QGCLabel { - text: qsTr("North (m)") - Layout.preferredWidth: _labelWidth - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - } - - QGCTextField { - id: northField - text: "0" - Layout.preferredWidth: _textFieldWidth - Layout.fillWidth: true - inputMethodHints: Qt.ImhPreferNumbers - validator: DoubleValidator { - notation: DoubleValidator.StandardNotation - locale: Qt.locale() - } - } - } - - RowLayout { - spacing: _margin - Layout.fillWidth: true - - QGCLabel { - text: qsTr("Up (m)") - Layout.preferredWidth: _labelWidth - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - } - - QGCTextField { - id: upField - text: "0" - Layout.preferredWidth: _textFieldWidth - Layout.fillWidth: true - inputMethodHints: Qt.ImhPreferNumbers - validator: DoubleValidator { - notation: DoubleValidator.StandardNotation - locale: Qt.locale() - } - } - } - - ColumnLayout { - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - - QGCCheckBox { - id: offsetTakeoffCheck - text: qsTr("Also move takeoff items") - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - } - - QGCCheckBox { - id: offsetLandingCheck - text: qsTr("Also move landing items") - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - } - } - - QGCLabel { - Layout.fillWidth: true - Layout.maximumWidth: _labelWidth + _textFieldWidth - - wrapMode: Text.WordWrap - font.pointSize: ScreenTools.smallFontPointSize - - text: qsTr("Note: Home altitude is not modified.") - } - } - - onAccepted: { - var east = Number.fromLocaleString(Qt.locale(), eastField.text) - var north = Number.fromLocaleString(Qt.locale(), northField.text) - var up = Number.fromLocaleString(Qt.locale(), upField.text) - - if (isNaN(east)) east = 0 - if (isNaN(north)) north = 0 - if (isNaN(up)) up = 0 - - _missionController.offsetMission( - east, - north, - up, - offsetTakeoffCheck.checked, - offsetLandingCheck.checked - ) - } - } - } - - Component { - id: rotateMissionDialogComponent - - QGCPopupDialog { - id: root - title: qsTr("Rotate Mission") - buttons: Dialog.Cancel | Dialog.Ok - modal: true - - property real _margin: ScreenTools.defaultFontPixelWidth / 2 - property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 20 - property real _labelWidth: ScreenTools.defaultFontPixelWidth * 14 - - ColumnLayout { - spacing: _margin - - RowLayout { - spacing: _margin - Layout.fillWidth: true - - QGCLabel { - text: qsTr("Clockwise (°)") - Layout.preferredWidth: _labelWidth - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - } - - QGCTextField { - id: degreesCWField - text: "0" - Layout.preferredWidth: _textFieldWidth - Layout.fillWidth: true - inputMethodHints: Qt.ImhPreferNumbers - validator: DoubleValidator { - notation: DoubleValidator.StandardNotation - locale: Qt.locale() - } - } - } - - ColumnLayout { - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - - QGCCheckBox { - id: rotateTakeoffCheck - text: qsTr("Also move takeoff items") - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - } - - QGCCheckBox { - id: rotateLandingCheck - text: qsTr("Also move landing items") - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - } - } - - QGCLabel { - Layout.fillWidth: true - Layout.maximumWidth: _labelWidth + _textFieldWidth - - wrapMode: Text.WordWrap - font.pointSize: ScreenTools.smallFontPointSize - - text: qsTr("Note: Complex items are rotated by moving their reference coordinate: their geometry and orientation are not changed.") - } - } - - onAccepted: { - var degreesCW = Number.fromLocaleString(Qt.locale(), degreesCWField.text) - - if (isNaN(degreesCW)) degreesCW = 0 - - _missionController.rotateMission( - degreesCW, - rotateTakeoffCheck.checked, - rotateLandingCheck.checked - ) - } - } - } - - Component { - id: editMissionPositionDialogComponent - - EditPositionDialog { - id: missionEditPositionDialog - - coordinate: _missionController.plannedHomePosition - onCoordinateChanged: _missionController.repositionMission(coordinate) - } - } - - QGCColoredImage { id: hamburger anchors.margins: _margin @@ -550,7 +281,7 @@ Rectangle { height: _hamburgerSize sourceSize.height: _hamburgerSize source: "qrc:/qmlimages/Hamburger.svg" - visible: missionItem.isCurrentItem + visible: missionItem.isCurrentItem && missionItem.sequenceNumber !== 0 color: qgcPal.buttonHighlightText QGCMouseArea { @@ -561,10 +292,7 @@ Rectangle { // For some strange reason using mainWindow in mapToItem doesn't work, so we use globals.parent instead which also gets us mainWindow position = mapToItem(globals.parent, position) - var panelComponent = (missionItem.sequenceNumber === 0) - ? missionStartHamburgerMenuDropPanelComponent - : hamburgerMenuDropPanelComponent - var dropPanel = panelComponent.createObject(mainWindow, { clickRect: Qt.rect(position.x, position.y, 0, 0) }) + var dropPanel = hamburgerMenuDropPanelComponent.createObject(mainWindow, { clickRect: Qt.rect(position.x, position.y, 0, 0) }) dropPanel.open() } } diff --git a/src/QmlControls/MissionItemTreeView.qml b/src/QmlControls/MissionItemTreeView.qml index 17368b4443ed..04e3746e0362 100644 --- a/src/QmlControls/MissionItemTreeView.qml +++ b/src/QmlControls/MissionItemTreeView.qml @@ -132,12 +132,14 @@ TreeView { case "missionGroup": return groupHeaderComponent case "fenceGroup": return groupHeaderComponent case "rallyGroup": return groupHeaderComponent + case "transformGroup": return groupHeaderComponent case "planFileInfo": return planFileInfoComponent case "defaultsInfo": return defaultsEditorComponent case "missionItem": return delegateRoot.nodeObject ? missionItemComponent : null case "fenceEditor": return delegateRoot.nodeObject ? fenceEditorComponent : null case "rallyHeader": return delegateRoot.nodeObject ? rallyHeaderComponent : null case "rallyItem": return delegateRoot.nodeObject ? rallyItemComponent : null + case "transformEditor": return transformEditorComponent default: return null } } @@ -513,5 +515,15 @@ TreeView { controller: root._rallyPointController } } + + // ── Transform editor (single child of transform group) ── + Component { + id: transformEditorComponent + + TransformEditor { + width: delegateRoot.width + missionController: root._missionController + } + } } } diff --git a/src/QmlControls/TransformEditor.qml b/src/QmlControls/TransformEditor.qml new file mode 100644 index 000000000000..f990ec517a3e --- /dev/null +++ b/src/QmlControls/TransformEditor.qml @@ -0,0 +1,336 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls + +Rectangle { + id: _root + + required property var missionController + + width: parent ? parent.width : 0 + height: mainColumn.height + (_margins * 2) + color: QGroundControl.globalPalette.windowShadeDark + + property real _margins: ScreenTools.defaultFontPixelWidth / 2 + property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 20 + property real _labelWidth: ScreenTools.defaultFontPixelWidth * 14 + property bool _hasHome: missionController ? missionController.plannedHomePosition.isValid : false + + TransformPositionController { + id: positionController + Component.onCompleted: { + if (_hasHome) { + coordinate = _root.missionController.plannedHomePosition + initValues() + } + } + } + + Connections { + target: _root.missionController + function onPlannedHomePositionChanged() { + positionController.coordinate = _root.missionController.plannedHomePosition + positionController.initValues() + } + } + + ColumnLayout { + id: mainColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: _margins + spacing: ScreenTools.defaultFontPixelHeight * 0.5 + + // ── Offset Mission ── + SectionHeader { + id: offsetSection + Layout.fillWidth: true + text: qsTr("Offset Mission") + checked: false + } + + ColumnLayout { + Layout.fillWidth: true + spacing: _margins + visible: offsetSection.checked + + LabelledFactTextField { + id: eastField + label: qsTr("East") + fact: positionController.offsetEast + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + } + + LabelledFactTextField { + id: northField + label: qsTr("North") + fact: positionController.offsetNorth + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + } + + LabelledFactTextField { + id: upField + label: qsTr("Up") + fact: positionController.offsetUp + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + } + + QGCCheckBox { + id: offsetTakeoffCheck + text: qsTr("Also move takeoff items") + } + + QGCCheckBox { + id: offsetLandingCheck + text: qsTr("Also move landing items") + } + + QGCLabel { + Layout.fillWidth: true + Layout.maximumWidth: _labelWidth + _textFieldWidth + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Note: Home altitude is not modified.") + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Apply Offset") + enabled: !eastField.textField.validationError + && !northField.textField.validationError + && !upField.textField.validationError + + onClicked: { + _root.missionController.offsetMission( + positionController.offsetEast.rawValue, + positionController.offsetNorth.rawValue, + positionController.offsetUp.rawValue, + offsetTakeoffCheck.checked, + offsetLandingCheck.checked + ) + } + } + } + + // ── Reposition Mission ── + SectionHeader { + id: repositionSection + Layout.fillWidth: true + text: qsTr("Reposition Mission") + checked: false + } + + ColumnLayout { + id: repositionContent + Layout.fillWidth: true + spacing: _margins + visible: repositionSection.checked + + QGCLabel { + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Home position must be set to reposition the mission.") + visible: !_hasHome + } + + property bool _showGeographic: coordinateSystemCombo.currentIndex === 0 + property bool _showUTM: coordinateSystemCombo.currentIndex === 1 + property bool _showMGRS: coordinateSystemCombo.currentIndex === 2 + property bool _showVehicle: coordinateSystemCombo.currentIndex === 3 + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + QGCLabel { + text: qsTr("Coordinate System") + } + + QGCComboBox { + id: coordinateSystemCombo + Layout.fillWidth: true + model: globals.activeVehicle + ? [ qsTr("Geographic"), qsTr("Universal Transverse Mercator"), qsTr("Military Grid Reference"), qsTr("Vehicle Position") ] + : [ qsTr("Geographic"), qsTr("Universal Transverse Mercator"), qsTr("Military Grid Reference") ] + } + } + + LabelledFactTextField { + id: latitudeField + label: qsTr("Latitude") + fact: positionController.latitude + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showGeographic + } + + LabelledFactTextField { + id: longitudeField + label: qsTr("Longitude") + fact: positionController.longitude + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showGeographic + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Move to Position") + enabled: _hasHome && !latitudeField.textField.validationError && !longitudeField.textField.validationError + visible: repositionContent._showGeographic + onClicked: { + positionController.setFromGeo() + _root.missionController.repositionMission(positionController.coordinate) + } + } + + LabelledFactTextField { + id: zoneField + label: qsTr("Zone") + fact: positionController.zone + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showUTM + } + + LabelledFactComboBox { + label: qsTr("Hemisphere") + fact: positionController.hemisphere + indexModel: false + Layout.fillWidth: true + visible: repositionContent._showUTM + } + + LabelledFactTextField { + id: eastingField + label: qsTr("Easting") + fact: positionController.easting + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showUTM + } + + LabelledFactTextField { + id: northingField + label: qsTr("Northing") + fact: positionController.northing + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showUTM + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Move to Position") + enabled: _hasHome && !zoneField.textField.validationError && !eastingField.textField.validationError && !northingField.textField.validationError + visible: repositionContent._showUTM + onClicked: { + positionController.setFromUTM() + _root.missionController.repositionMission(positionController.coordinate) + } + } + + LabelledFactTextField { + id: mgrsField + label: qsTr("MGRS") + fact: positionController.mgrs + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showMGRS + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Move to Position") + enabled: _hasHome && !mgrsField.textField.validationError + visible: repositionContent._showMGRS + onClicked: { + positionController.setFromMGRS() + _root.missionController.repositionMission(positionController.coordinate) + } + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Move to Vehicle Position") + enabled: _hasHome + visible: repositionContent._showVehicle + onClicked: { + positionController.setFromVehicle() + _root.missionController.repositionMission(positionController.coordinate) + } + } + } + + // ── Rotate Mission ── + SectionHeader { + id: rotateSection + Layout.fillWidth: true + text: qsTr("Rotate Mission") + checked: false + } + + ColumnLayout { + Layout.fillWidth: true + spacing: _margins + visible: rotateSection.checked + + QGCLabel { + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Home position must be set to rotate the mission.") + visible: !_hasHome + } + + LabelledFactTextField { + id: degreesCWField + label: qsTr("Clockwise") + fact: positionController.rotateDegreesCW + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + } + + QGCCheckBox { + id: rotateTakeoffCheck + text: qsTr("Also move takeoff items") + } + + QGCCheckBox { + id: rotateLandingCheck + text: qsTr("Also move landing items") + } + + QGCLabel { + Layout.fillWidth: true + Layout.maximumWidth: _labelWidth + _textFieldWidth + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Note: Complex items are rotated by moving their reference coordinate: their geometry and orientation are not changed.") + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Apply Rotation") + enabled: _hasHome && !degreesCWField.textField.validationError + + onClicked: { + _root.missionController.rotateMission( + positionController.rotateDegreesCW.rawValue, + rotateTakeoffCheck.checked, + rotateLandingCheck.checked + ) + } + } + } + } +} diff --git a/src/QmlControls/EditPositionDialog.FactMetaData.json b/src/QmlControls/TransformPositionController.FactMetaData.json similarity index 56% rename from src/QmlControls/EditPositionDialog.FactMetaData.json rename to src/QmlControls/TransformPositionController.FactMetaData.json index afc198d3097f..32645461c90d 100644 --- a/src/QmlControls/EditPositionDialog.FactMetaData.json +++ b/src/QmlControls/TransformPositionController.FactMetaData.json @@ -7,6 +7,7 @@ "name": "Latitude", "shortDesc": "Latitude of item position", "type": "double", + "units": "deg", "min": -90.0, "max": 90.0, "decimalPlaces": 7 @@ -15,6 +16,7 @@ "name": "Longitude", "shortDesc": "Longitude of item position", "type": "double", + "units": "deg", "min": -180.0, "max": 180.0, "decimalPlaces": 7 @@ -23,13 +25,13 @@ "name": "Easting", "shortDesc": "Easting of item position", "type": "double", - "decimalPlaces": 7 + "decimalPlaces": 2 }, { "name": "Northing", "shortDesc": "Northing of item position", "type": "double", - "decimalPlaces": 7 + "decimalPlaces": 2 }, { "name": "Zone", @@ -49,6 +51,38 @@ "name": "MGRS", "shortDesc": "MGRS coordinate", "type": "string" +}, +{ + "name": "OffsetEast", + "shortDesc": "East offset", + "type": "double", + "units": "m", + "defaultValue": 0, + "decimalPlaces": 2 +}, +{ + "name": "OffsetNorth", + "shortDesc": "North offset", + "type": "double", + "units": "m", + "defaultValue": 0, + "decimalPlaces": 2 +}, +{ + "name": "OffsetUp", + "shortDesc": "Up offset", + "type": "double", + "units": "m", + "defaultValue": 0, + "decimalPlaces": 2 +}, +{ + "name": "RotateDegreesCW", + "shortDesc": "Clockwise rotation", + "type": "double", + "units": "deg", + "defaultValue": 0, + "decimalPlaces": 1 } ] } diff --git a/src/QmlControls/EditPositionDialogController.cc b/src/QmlControls/TransformPositionController.cc similarity index 56% rename from src/QmlControls/EditPositionDialogController.cc rename to src/QmlControls/TransformPositionController.cc index d26d9a847877..991b288aa245 100644 --- a/src/QmlControls/EditPositionDialogController.cc +++ b/src/QmlControls/TransformPositionController.cc @@ -1,14 +1,14 @@ -#include "EditPositionDialogController.h" +#include "TransformPositionController.h" #include "QGCGeo.h" #include "MultiVehicleManager.h" #include "Vehicle.h" #include "QGCLoggingCategory.h" -QGC_LOGGING_CATEGORY(EditPositionDialogControllerLog, "QMLControls.EditPositionDialogController") +QGC_LOGGING_CATEGORY(TransformPositionControllerLog, "QMLControls.TransformPositionController") -QMap EditPositionDialogController::_metaDataMap; +QMap TransformPositionController::_metaDataMap; -EditPositionDialogController::EditPositionDialogController(QObject *parent) +TransformPositionController::TransformPositionController(QObject *parent) : QObject(parent) , _latitudeFact(new Fact(0, _latitudeFactName, FactMetaData::valueTypeDouble, this)) , _longitudeFact(new Fact(0, _longitudeFactName, FactMetaData::valueTypeDouble, this)) @@ -17,11 +17,15 @@ EditPositionDialogController::EditPositionDialogController(QObject *parent) , _eastingFact(new Fact(0, _eastingFactName, FactMetaData::valueTypeDouble, this)) , _northingFact(new Fact(0, _northingFactName, FactMetaData::valueTypeDouble, this)) , _mgrsFact(new Fact(0, _mgrsFactName, FactMetaData::valueTypeString, this)) + , _offsetEastFact(new Fact(0, _offsetEastFactName, FactMetaData::valueTypeDouble, this)) + , _offsetNorthFact(new Fact(0, _offsetNorthFactName, FactMetaData::valueTypeDouble, this)) + , _offsetUpFact(new Fact(0, _offsetUpFactName, FactMetaData::valueTypeDouble, this)) + , _rotateDegreesCWFact(new Fact(0, _rotateDegreesCWFactName, FactMetaData::valueTypeDouble, this)) { - // qCDebug(EditPositionDialogControllerLog) << Q_FUNC_INFO << this; + // qCDebug(TransformPositionControllerLog) << Q_FUNC_INFO << this; if (_metaDataMap.isEmpty()) { - _metaDataMap = FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/EditPositionDialog.FactMetaData.json"), nullptr /* QObject parent */); + _metaDataMap = FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/TransformPositionController.FactMetaData.json"), nullptr /* QObject parent */); } _latitudeFact->setMetaData(_metaDataMap[_latitudeFactName]); @@ -31,17 +35,21 @@ EditPositionDialogController::EditPositionDialogController(QObject *parent) _eastingFact->setMetaData(_metaDataMap[_eastingFactName]); _northingFact->setMetaData(_metaDataMap[_northingFactName]); _mgrsFact->setMetaData(_metaDataMap[_mgrsFactName]); + _offsetEastFact->setMetaData(_metaDataMap[_offsetEastFactName]); + _offsetNorthFact->setMetaData(_metaDataMap[_offsetNorthFactName]); + _offsetUpFact->setMetaData(_metaDataMap[_offsetUpFactName]); + _rotateDegreesCWFact->setMetaData(_metaDataMap[_rotateDegreesCWFactName]); } -EditPositionDialogController::~EditPositionDialogController() +TransformPositionController::~TransformPositionController() { - // qCDebug(EditPositionDialogControllerLog) << Q_FUNC_INFO << this; + // qCDebug(TransformPositionControllerLog) << Q_FUNC_INFO << this; } -void EditPositionDialogController::setCoordinate(QGeoCoordinate coordinate) +void TransformPositionController::setCoordinate(QGeoCoordinate coordinate) { if (!coordinate.isValid()) { - qCWarning(EditPositionDialogControllerLog) << "Attempt to set invalid coordinate"; + qCWarning(TransformPositionControllerLog) << "Attempt to set invalid coordinate"; return; } @@ -59,7 +67,7 @@ void EditPositionDialogController::setCoordinate(QGeoCoordinate coordinate) } } -void EditPositionDialogController::initValues() +void TransformPositionController::initValues() { if (!_coordinate.isValid()) { return; @@ -83,7 +91,7 @@ void EditPositionDialogController::initValues() } } -void EditPositionDialogController::setFromGeo() +void TransformPositionController::setFromGeo() { QGeoCoordinate newCoordinate = _coordinate; newCoordinate.setLatitude(_latitudeFact->rawValue().toDouble()); @@ -91,19 +99,19 @@ void EditPositionDialogController::setFromGeo() setCoordinate(newCoordinate); } -void EditPositionDialogController::setFromUTM() +void TransformPositionController::setFromUTM() { - qCDebug(EditPositionDialogControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1); + qCDebug(TransformPositionControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1); QGeoCoordinate newCoordinate; if (QGCGeo::convertUTMToGeo(_eastingFact->rawValue().toDouble(), _northingFact->rawValue().toDouble(), _zoneFact->rawValue().toInt(), _hemisphereFact->rawValue().toInt() == 1, newCoordinate)) { - qCDebug(EditPositionDialogControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1) << newCoordinate; + qCDebug(TransformPositionControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1) << newCoordinate; setCoordinate(newCoordinate); } else { initValues(); } } -void EditPositionDialogController::setFromMGRS() +void TransformPositionController::setFromMGRS() { QGeoCoordinate newCoordinate; if (QGCGeo::convertMGRSToGeo(_mgrsFact->rawValue().toString(), newCoordinate)) { @@ -113,7 +121,13 @@ void EditPositionDialogController::setFromMGRS() } } -void EditPositionDialogController::setFromVehicle() +void TransformPositionController::setFromVehicle() { - setCoordinate(MultiVehicleManager::instance()->activeVehicle()->coordinate()); + Vehicle* activeVehicle = MultiVehicleManager::instance()->activeVehicle(); + if (!activeVehicle) { + qCWarning(TransformPositionControllerLog) << "Cannot set coordinate from vehicle: no active vehicle"; + return; + } + + setCoordinate(activeVehicle->coordinate()); } diff --git a/src/QmlControls/TransformPositionController.h b/src/QmlControls/TransformPositionController.h new file mode 100644 index 000000000000..e2ef4bbaf45c --- /dev/null +++ b/src/QmlControls/TransformPositionController.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include + +#include "Fact.h" + +Q_DECLARE_LOGGING_CATEGORY(TransformPositionControllerLog) + +class TransformPositionController : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged) + Q_PROPERTY(Fact *latitude READ latitude CONSTANT) + Q_PROPERTY(Fact *longitude READ longitude CONSTANT) + Q_PROPERTY(Fact *zone READ zone CONSTANT) + Q_PROPERTY(Fact *hemisphere READ hemisphere CONSTANT) + Q_PROPERTY(Fact *easting READ easting CONSTANT) + Q_PROPERTY(Fact *northing READ northing CONSTANT) + Q_PROPERTY(Fact *mgrs READ mgrs CONSTANT) + Q_PROPERTY(Fact *offsetEast READ offsetEast CONSTANT) + Q_PROPERTY(Fact *offsetNorth READ offsetNorth CONSTANT) + Q_PROPERTY(Fact *offsetUp READ offsetUp CONSTANT) + Q_PROPERTY(Fact *rotateDegreesCW READ rotateDegreesCW CONSTANT) + +public: + explicit TransformPositionController(QObject *parent = nullptr); + ~TransformPositionController(); + + Q_INVOKABLE void initValues(); + Q_INVOKABLE void setFromGeo(); + Q_INVOKABLE void setFromUTM(); + Q_INVOKABLE void setFromMGRS(); + Q_INVOKABLE void setFromVehicle(); + + void setCoordinate(QGeoCoordinate coordinate); + QGeoCoordinate coordinate() const { return _coordinate; } + + Fact *latitude() { return _latitudeFact; } + Fact *longitude() { return _longitudeFact; } + Fact *zone() { return _zoneFact; } + Fact *hemisphere() { return _hemisphereFact; } + Fact *easting() { return _eastingFact; } + Fact *northing() { return _northingFact; } + Fact *mgrs() { return _mgrsFact; } + Fact *offsetEast() { return _offsetEastFact; } + Fact *offsetNorth() { return _offsetNorthFact; } + Fact *offsetUp() { return _offsetUpFact; } + Fact *rotateDegreesCW() { return _rotateDegreesCWFact; } + +signals: + void coordinateChanged(QGeoCoordinate coordinate); + +private: + QGeoCoordinate _coordinate; + + Fact *_latitudeFact = nullptr; + Fact *_longitudeFact = nullptr; + Fact *_zoneFact = nullptr; + Fact *_hemisphereFact = nullptr; + Fact *_eastingFact = nullptr; + Fact *_northingFact = nullptr; + Fact *_mgrsFact = nullptr; + Fact *_offsetEastFact = nullptr; + Fact *_offsetNorthFact = nullptr; + Fact *_offsetUpFact = nullptr; + Fact *_rotateDegreesCWFact = nullptr; + + static QMap _metaDataMap; + + static constexpr const char *_latitudeFactName = "Latitude"; + static constexpr const char *_longitudeFactName = "Longitude"; + static constexpr const char *_zoneFactName = "Zone"; + static constexpr const char *_hemisphereFactName = "Hemisphere"; + static constexpr const char *_eastingFactName = "Easting"; + static constexpr const char *_northingFactName = "Northing"; + static constexpr const char *_mgrsFactName = "MGRS"; + static constexpr const char *_offsetEastFactName = "OffsetEast"; + static constexpr const char *_offsetNorthFactName = "OffsetNorth"; + static constexpr const char *_offsetUpFactName = "OffsetUp"; + static constexpr const char *_rotateDegreesCWFactName = "RotateDegreesCW"; +}; From 13e87fee1ce0b5a09fa30dfd515cb394833f0e44 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 8 Mar 2026 00:29:52 +0000 Subject: [PATCH 051/119] New Crowdin translations by GitHub Action --- .../fly_view/fly_view_toolbar.md | 66 ++++++++++++++----- .../fly_view/fly_view_toolbar.md | 66 ++++++++++++++----- .../fly_view/fly_view_toolbar.md | 66 ++++++++++++++----- 3 files changed, 147 insertions(+), 51 deletions(-) diff --git a/docs/ko/qgc-user-guide/fly_view/fly_view_toolbar.md b/docs/ko/qgc-user-guide/fly_view/fly_view_toolbar.md index d4a3ea9ed6d4..0adbc5390dba 100644 --- a/docs/ko/qgc-user-guide/fly_view/fly_view_toolbar.md +++ b/docs/ko/qgc-user-guide/fly_view/fly_view_toolbar.md @@ -13,11 +13,11 @@ The "Q" icon on the left of the toolbar allows you to select between additional ## Toolbar Indicators -Next are a multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. +Next are multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. ![Toolbar Indicator - expand button](../../../assets/fly/toolbar_indicator_expand.png) -### Flight Status +### Flight Status Flight Status indicator The Flight Status indicator shows you whether the vehicle is ready to fly or not. It can be in one of the following states: @@ -29,10 +29,10 @@ The Flight Status indicator shows you whether the vehicle is ready to fly or not - **Landing** - Vehicle is in the process of landing. - **Communication Lost** - QGroundControl has lost communication with the vehicle. -The Flight Status Indicator dropdown also gives you acess to: +The Flight Status indicator dropdown also gives you access to: - **Arm** - Arming a vehicle starts the motors in preparation for takeoff. You will only be able to arm the vehicle if it is safe and ready to fly. Generally you do not need to manually arm the vehicle. You can simply takeoff or start a mission and the vehicle will arm itself. -- **Disarm** - Disarming a vehicle is only availble when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. +- **Disarm** - Disarming a vehicle is only available when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. - **Emergency Stop** - Emergency stop is used to disarm the vehicle while it is flying. For emergency use only, your vehicle will crash! In the cases of warnings or not ready state you can click the indicator to display the dropdown which will show the reason(s) why. The toggle on the right expands each error with additional information and possible solutions. @@ -41,7 +41,7 @@ In the cases of warnings or not ready state you can click the indicator to displ Once each issue is resolved it will disappear from the UI. When all issues blocking arming have been removed you should now be ready to fly. -## Flight Mode +### Flight Mode Flight Mode indicator The Flight Mode indicator shows you the current flight mode. The dropdown allows you to switch between flight modes. The expanded page allows you to: @@ -49,15 +49,23 @@ The Flight Mode indicator shows you the current flight mode. The dropdown allows - Set global geo-fence settings - Add/Remove flight modes from the displayed list -## Vehicle Messages +### Vehicle Messages Vehicle Messages indicator The Vehicle Messages indicator dropdown shows you messages which come from the vehicle. The indicator will turn red if there are important messages available. -## GPS +### GPS / RTK GPS GPS / RTK GPS indicator -The GPS indicator shows you the satellite count and the HDOP in the toolbar icon. The dropdown shows you additional GPS status. The expanded page give you access to RTK settings. +The GPS/RTK GPS indicator shows satellite and GNSS status in the toolbar, and the dropdown provides additional GPS details. -## Battery +With an active vehicle, the indicator shows vehicle GPS information (for example, satellite count and HDOP), and the expanded page provides access to RTK-related settings. + +When there is no active vehicle but RTK is connected, the indicator switches to RTK status so you can still monitor the correction link. + +### GPS Resilience + +The GPS Resilience indicator appears when the vehicle reports GPS resilience telemetry (authentication, spoofing, or jamming state). The dropdown provides summary status and per-GPS details when available. + +### Battery Battery indicator The Battery indicator shows you a configurable colored battery icon for remaining charge. It can also be configured to show percent remaining, voltage or both. The expanded page allows you to: @@ -65,14 +73,38 @@ The Battery indicator shows you a configurable colored battery icon for remainin - Configure the icon coloring - Set up the low battery failsafe -## Remote ID +### Remote ID Remote ID indicator + +The Remote ID indicator appears when Remote ID is available on the active vehicle. Its color indicates overall Remote ID health, and the dropdown shows Remote ID status details. + +### ESC ESC indicator + +The ESC indicator appears when ESC telemetry is available from the vehicle. It shows overall ESC health and online motor count, and opens a detailed ESC status page. + +### Joystick Joystick indicator + +The Joystick indicator appears when a joystick/gamepad is detected. The dropdown shows device status and connection details. + +### Telemetry RSSI Telemetry RSSI indicator + +The Telemetry RSSI indicator appears when telemetry signal information is available. It provides local/remote RSSI and additional radio link quality details. + +### RC RSSI RC RSSI indicator + +The RC RSSI indicator appears when RC signal information is available. It shows current RC link strength and opens a page with RC RSSI details. + +### Gimbal Gimbal indicator + +The Gimbal indicator is shown when the vehicle supports the [MAVLink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html). It displays active gimbal status and provides access to gimbal controls and settings. + +### VTOL Transitions VTOL indicator + +For VTOL vehicles, a VTOL transition status indicator is shown when applicable. It indicates the current VTOL mode/state and provides transition-related status information. + +### Multi-Vehicle Selector Multi-Vehicle indicator -## Other Indicators +The Multi-Vehicle selector appears when more than one vehicle is connected. It allows you to quickly switch the active vehicle from the toolbar. -There are other indicators which only show in certain situations: +### APM Support Forwarding APM Support Forwarding indicator -- Telemetry RSSI -- RC RSSI -- Gimbal - Only displayed if the vehicle supports the [Mavlink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html) -- VTOL transitions -- Select from multiple connected vehicles +On ArduPilot, an APM Support Forwarding indicator appears when MAVLink traffic forwarding to a support server is enabled. diff --git a/docs/tr/qgc-user-guide/fly_view/fly_view_toolbar.md b/docs/tr/qgc-user-guide/fly_view/fly_view_toolbar.md index d4a3ea9ed6d4..0adbc5390dba 100644 --- a/docs/tr/qgc-user-guide/fly_view/fly_view_toolbar.md +++ b/docs/tr/qgc-user-guide/fly_view/fly_view_toolbar.md @@ -13,11 +13,11 @@ The "Q" icon on the left of the toolbar allows you to select between additional ## Toolbar Indicators -Next are a multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. +Next are multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. ![Toolbar Indicator - expand button](../../../assets/fly/toolbar_indicator_expand.png) -### Flight Status +### Flight Status Flight Status indicator The Flight Status indicator shows you whether the vehicle is ready to fly or not. It can be in one of the following states: @@ -29,10 +29,10 @@ The Flight Status indicator shows you whether the vehicle is ready to fly or not - **Landing** - Vehicle is in the process of landing. - **Communication Lost** - QGroundControl has lost communication with the vehicle. -The Flight Status Indicator dropdown also gives you acess to: +The Flight Status indicator dropdown also gives you access to: - **Arm** - Arming a vehicle starts the motors in preparation for takeoff. You will only be able to arm the vehicle if it is safe and ready to fly. Generally you do not need to manually arm the vehicle. You can simply takeoff or start a mission and the vehicle will arm itself. -- **Disarm** - Disarming a vehicle is only availble when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. +- **Disarm** - Disarming a vehicle is only available when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. - **Emergency Stop** - Emergency stop is used to disarm the vehicle while it is flying. For emergency use only, your vehicle will crash! In the cases of warnings or not ready state you can click the indicator to display the dropdown which will show the reason(s) why. The toggle on the right expands each error with additional information and possible solutions. @@ -41,7 +41,7 @@ In the cases of warnings or not ready state you can click the indicator to displ Once each issue is resolved it will disappear from the UI. When all issues blocking arming have been removed you should now be ready to fly. -## Flight Mode +### Flight Mode Flight Mode indicator The Flight Mode indicator shows you the current flight mode. The dropdown allows you to switch between flight modes. The expanded page allows you to: @@ -49,15 +49,23 @@ The Flight Mode indicator shows you the current flight mode. The dropdown allows - Set global geo-fence settings - Add/Remove flight modes from the displayed list -## Vehicle Messages +### Vehicle Messages Vehicle Messages indicator The Vehicle Messages indicator dropdown shows you messages which come from the vehicle. The indicator will turn red if there are important messages available. -## GPS +### GPS / RTK GPS GPS / RTK GPS indicator -The GPS indicator shows you the satellite count and the HDOP in the toolbar icon. The dropdown shows you additional GPS status. The expanded page give you access to RTK settings. +The GPS/RTK GPS indicator shows satellite and GNSS status in the toolbar, and the dropdown provides additional GPS details. -## Battery +With an active vehicle, the indicator shows vehicle GPS information (for example, satellite count and HDOP), and the expanded page provides access to RTK-related settings. + +When there is no active vehicle but RTK is connected, the indicator switches to RTK status so you can still monitor the correction link. + +### GPS Resilience + +The GPS Resilience indicator appears when the vehicle reports GPS resilience telemetry (authentication, spoofing, or jamming state). The dropdown provides summary status and per-GPS details when available. + +### Battery Battery indicator The Battery indicator shows you a configurable colored battery icon for remaining charge. It can also be configured to show percent remaining, voltage or both. The expanded page allows you to: @@ -65,14 +73,38 @@ The Battery indicator shows you a configurable colored battery icon for remainin - Configure the icon coloring - Set up the low battery failsafe -## Remote ID +### Remote ID Remote ID indicator + +The Remote ID indicator appears when Remote ID is available on the active vehicle. Its color indicates overall Remote ID health, and the dropdown shows Remote ID status details. + +### ESC ESC indicator + +The ESC indicator appears when ESC telemetry is available from the vehicle. It shows overall ESC health and online motor count, and opens a detailed ESC status page. + +### Joystick Joystick indicator + +The Joystick indicator appears when a joystick/gamepad is detected. The dropdown shows device status and connection details. + +### Telemetry RSSI Telemetry RSSI indicator + +The Telemetry RSSI indicator appears when telemetry signal information is available. It provides local/remote RSSI and additional radio link quality details. + +### RC RSSI RC RSSI indicator + +The RC RSSI indicator appears when RC signal information is available. It shows current RC link strength and opens a page with RC RSSI details. + +### Gimbal Gimbal indicator + +The Gimbal indicator is shown when the vehicle supports the [MAVLink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html). It displays active gimbal status and provides access to gimbal controls and settings. + +### VTOL Transitions VTOL indicator + +For VTOL vehicles, a VTOL transition status indicator is shown when applicable. It indicates the current VTOL mode/state and provides transition-related status information. + +### Multi-Vehicle Selector Multi-Vehicle indicator -## Other Indicators +The Multi-Vehicle selector appears when more than one vehicle is connected. It allows you to quickly switch the active vehicle from the toolbar. -There are other indicators which only show in certain situations: +### APM Support Forwarding APM Support Forwarding indicator -- Telemetry RSSI -- RC RSSI -- Gimbal - Only displayed if the vehicle supports the [Mavlink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html) -- VTOL transitions -- Select from multiple connected vehicles +On ArduPilot, an APM Support Forwarding indicator appears when MAVLink traffic forwarding to a support server is enabled. diff --git a/docs/zh/qgc-user-guide/fly_view/fly_view_toolbar.md b/docs/zh/qgc-user-guide/fly_view/fly_view_toolbar.md index ba1854dcfc8a..9d3009f2d1ba 100644 --- a/docs/zh/qgc-user-guide/fly_view/fly_view_toolbar.md +++ b/docs/zh/qgc-user-guide/fly_view/fly_view_toolbar.md @@ -13,11 +13,11 @@ ## 工具栏指示器 -接下来是关于车辆状况的多个工具栏指标。 每个工具栏指示器的下拉功能提供了关于状态的更多细节。 您也可以展开指示器以显示与指示器相关联的其他应用程序和载具设置。 按">"按钮来扩展。 +Next are multiple toolbar indicators for vehicle status. 每个工具栏指示器的下拉功能提供了关于状态的更多细节。 您也可以展开指示器以显示与指示器相关联的其他应用程序和载具设置。 按">"按钮来扩展。 ![工具栏指示器 - 展开按钮](../../../assets/fly/toolbar_indicator_expand.png) -### 飞行状态 +### Flight Status Flight Status indicator 飞行状态指示器显示载具是否可以飞行。 它可以是下列状态之一: @@ -29,10 +29,10 @@ - **着陆** - 载具正在着陆。 - **通信丢失** - QGroundControl已失去与载具的通信。 -飞行状态指示器下拉也让您可以: +The Flight Status indicator dropdown also gives you access to: - **解锁** - 解锁一辆载具开启发动机以准备起飞。 你只有在载具安全和准备飞行时才能解锁载具。 通常你不需要手动解锁载具。 你可以简单地起飞或开始执行任务,载具将解锁自己。 -- **锁定** - 只有当载具在地面时才能锁定载具。 它会关停电机。 一般来说,你无需明确进行锁定操作,因为飞行器会在着陆后自动锁定,或者如果在解锁后若未起飞,不久后也会自动锁定。 +- **Disarm** - Disarming a vehicle is only available when the vehicle is on the ground. 它会关停电机。 一般来说,你无需明确进行锁定操作,因为飞行器会在着陆后自动锁定,或者如果在解锁后若未起飞,不久后也会自动锁定。 - **紧急停机** - 在载具飞行时用紧急停机锁定载具。 仅供紧急情况使用,你的飞行器会坠毁! 在警告或尚未准备好状态的情况下,您可以点击指示器来显示下拉菜单,显示原因(s)。 右侧的切换按钮会展开每个错误,并显示更多信息及可能的解决方案。 @@ -41,7 +41,7 @@ 每个问题解决后,将从用户界面中消失。 当所有阻止解锁的问题被移除时,你现在应该可以准备飞行了。 -## 飞行模式 +### Flight Mode Flight Mode indicator 飞行模式指示器显示您当前的飞行模式。 下拉菜单允许您在飞行模式之间切换。 扩展页面允许您: @@ -49,15 +49,23 @@ - 设置全局地理栅栏设置 - 从显示列表中添加/删除飞行模式 -## 载具信息 +### Vehicle Messages Vehicle Messages indicator 车辆消息指示器下拉显示来自车辆的消息。 如果有重要信息,指示器将会变红。 -## GPS +### GPS / RTK GPS GPS / RTK GPS indicator -GPS指示器在工具栏图标中向您显示卫星计数和HDOP。 下拉菜单显示您额外的 GPS 状态。 扩展页面允许您访问RTK设置。 +The GPS/RTK GPS indicator shows satellite and GNSS status in the toolbar, and the dropdown provides additional GPS details. -## 电池 +With an active vehicle, the indicator shows vehicle GPS information (for example, satellite count and HDOP), and the expanded page provides access to RTK-related settings. + +When there is no active vehicle but RTK is connected, the indicator switches to RTK status so you can still monitor the correction link. + +### GPS Resilience + +The GPS Resilience indicator appears when the vehicle reports GPS resilience telemetry (authentication, spoofing, or jamming state). The dropdown provides summary status and per-GPS details when available. + +### Battery Battery indicator 电池指示器向您展示了一个可配置的充电图标。 它也可以配置为显示剩余的电压或两种电压。 扩展页面允许您: @@ -65,14 +73,38 @@ GPS指示器在工具栏图标中向您显示卫星计数和HDOP。 下拉菜单 - 配置图标着色 - 设置低电量故障安全 -## 远程 ID +### Remote ID Remote ID indicator + +The Remote ID indicator appears when Remote ID is available on the active vehicle. Its color indicates overall Remote ID health, and the dropdown shows Remote ID status details. + +### ESC ESC indicator + +The ESC indicator appears when ESC telemetry is available from the vehicle. It shows overall ESC health and online motor count, and opens a detailed ESC status page. + +### Joystick Joystick indicator + +The Joystick indicator appears when a joystick/gamepad is detected. The dropdown shows device status and connection details. + +### Telemetry RSSI Telemetry RSSI indicator + +The Telemetry RSSI indicator appears when telemetry signal information is available. It provides local/remote RSSI and additional radio link quality details. + +### RC RSSI RC RSSI indicator + +The RC RSSI indicator appears when RC signal information is available. It shows current RC link strength and opens a page with RC RSSI details. + +### Gimbal Gimbal indicator + +The Gimbal indicator is shown when the vehicle supports the [MAVLink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html). It displays active gimbal status and provides access to gimbal controls and settings. + +### VTOL Transitions VTOL indicator + +For VTOL vehicles, a VTOL transition status indicator is shown when applicable. It indicates the current VTOL mode/state and provides transition-related status information. + +### Multi-Vehicle Selector Multi-Vehicle indicator -## 其他指标 +The Multi-Vehicle selector appears when more than one vehicle is connected. It allows you to quickly switch the active vehicle from the toolbar. -还有其他指标只能在某些情况下显示: +### APM Support Forwarding APM Support Forwarding indicator -- 遥测信号强度指示 -- 遥控信号强度指示 -- Gimbal - Only displayed if the vehicle supports the [Mavlink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html) -- 垂直起降转换 -- 从多个连接的载具中选择 +On ArduPilot, an APM Support Forwarding indicator appears when MAVLink traffic forwarding to a support server is enabled. From 2e1918d924d314f42f8160ced2b3c8c4ae6a9931 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 8 Mar 2026 10:00:20 -0700 Subject: [PATCH 052/119] Extract PlanView and Toolbar QML modules from QmlControls - Create new src/PlanView/ module (QGroundControl.PlanView) with 39 QML files for mission planning: editors, map visuals, and plan view UI - Move 14 toolbar QML files to existing src/UI/toolbar/ module (QGroundControl.Toolbar) - Update C++ qrc paths in MissionManager from Controls to PlanView - Add QML imports to all external consumers - Update CMake module declarations and linkage --- .../APM/APMFollowComponent.qml | 1 + src/CMakeLists.txt | 2 + .../MapItems/MissionItemIndicator.qml | 1 + src/FlightMap/MapItems/PlanMapItems.qml | 1 + .../MapItems/QGCMapPolygonVisuals.qml | 1 + .../MapItems/QGCMapPolylineVisuals.qml | 1 + src/FlyView/FlyView.qml | 1 + src/FlyView/FlyViewMap.qml | 1 + src/MissionManager/CorridorScanComplexItem.cc | 2 +- .../FixedWingLandingComplexItem.cc | 2 +- src/MissionManager/MissionSettingsItem.cc | 2 +- src/MissionManager/SimpleMissionItem.cc | 4 +- .../StructureScanComplexItem.cc | 2 +- src/MissionManager/SurveyComplexItem.cc | 2 +- src/MissionManager/TakeoffMissionItem.cc | 2 +- src/MissionManager/VTOLLandingComplexItem.cc | 2 +- src/PlanView/CMakeLists.txt | 53 +++++++++++++++ .../CameraCalcCamera.qml | 0 .../CameraCalcGrid.qml | 0 .../CameraSection.qml | 0 .../CorridorScanEditor.qml | 0 .../CorridorScanMapVisual.qml | 0 .../FWLandingPatternEditor.qml | 0 .../FWLandingPatternMapVisual.qml | 0 .../GeoFenceEditor.qml | 0 .../GeoFenceMapVisuals.qml | 0 .../MissionItemEditor.qml | 0 .../MissionItemIndexLabel.qml | 0 .../MissionItemMapVisual.qml | 0 .../MissionItemStatus.qml | 0 .../MissionItemTreeView.qml | 1 + .../MissionSettingsEditor.qml | 0 .../MissionStats.qml | 0 .../PlanEditToolbar.qml | 0 .../PlanToolBarIndicators.qml | 0 src/{QmlControls => PlanView}/PlanView.qml | 1 + .../PlanViewRightPanel.qml | 0 .../RallyPointEditorHeader.qml | 0 .../RallyPointItemEditor.qml | 0 .../RallyPointMapVisuals.qml | 0 .../SimpleItemEditor.qml | 0 .../SimpleItemMapVisual.qml | 0 .../StructureScanEditor.qml | 0 .../StructureScanMapVisual.qml | 0 .../SurveyItemEditor.qml | 0 .../SurveyMapVisual.qml | 0 .../TakeoffItemMapVisual.qml | 0 .../TerrainStatus.qml | 0 .../TransectStyleComplexItemEditor.qml | 0 .../TransectStyleComplexItemStats.qml | 0 .../TransectStyleComplexItemTabBar.qml | 0 .../TransectStyleComplexItemTerrainFollow.qml | 0 .../TransectStyleMapVisuals.qml | 0 .../TransformEditor.qml | 0 .../VTOLLandingPatternEditor.qml | 0 .../VTOLLandingPatternMapVisual.qml | 0 src/QmlControls/CMakeLists.txt | 65 ++----------------- src/UI/MainWindow.qml | 1 + .../toolbar}/BatteryIndicator.qml | 0 src/UI/toolbar/CMakeLists.txt | 14 ++++ .../toolbar}/FlightModeIndicator.qml | 0 .../toolbar}/FlightModeMenuIndicator.qml | 0 .../toolbar}/FlyViewToolBar.qml | 0 .../toolbar}/FlyViewToolBarIndicators.qml | 0 .../toolbar}/GPSIndicator.qml | 0 .../toolbar}/GPSIndicatorPage.qml | 0 .../toolbar}/MainStatusIndicator.qml | 0 .../MainStatusIndicatorOfflinePage.qml | 0 .../toolbar}/ParameterDownloadProgress.qml | 0 .../toolbar}/PlanViewToolBar.qml | 1 + .../toolbar}/RemoteIDIndicatorPage.qml | 0 .../toolbar}/SignalStrength.qml | 0 .../toolbar}/VehicleMessageList.qml | 0 73 files changed, 94 insertions(+), 69 deletions(-) create mode 100644 src/PlanView/CMakeLists.txt rename src/{QmlControls => PlanView}/CameraCalcCamera.qml (100%) rename src/{QmlControls => PlanView}/CameraCalcGrid.qml (100%) rename src/{QmlControls => PlanView}/CameraSection.qml (100%) rename src/{QmlControls => PlanView}/CorridorScanEditor.qml (100%) rename src/{QmlControls => PlanView}/CorridorScanMapVisual.qml (100%) rename src/{QmlControls => PlanView}/FWLandingPatternEditor.qml (100%) rename src/{QmlControls => PlanView}/FWLandingPatternMapVisual.qml (100%) rename src/{QmlControls => PlanView}/GeoFenceEditor.qml (100%) rename src/{QmlControls => PlanView}/GeoFenceMapVisuals.qml (100%) rename src/{QmlControls => PlanView}/MissionItemEditor.qml (100%) rename src/{QmlControls => PlanView}/MissionItemIndexLabel.qml (100%) rename src/{QmlControls => PlanView}/MissionItemMapVisual.qml (100%) rename src/{QmlControls => PlanView}/MissionItemStatus.qml (100%) rename src/{QmlControls => PlanView}/MissionItemTreeView.qml (99%) rename src/{QmlControls => PlanView}/MissionSettingsEditor.qml (100%) rename src/{QmlControls => PlanView}/MissionStats.qml (100%) rename src/{QmlControls => PlanView}/PlanEditToolbar.qml (100%) rename src/{QmlControls => PlanView}/PlanToolBarIndicators.qml (100%) rename src/{QmlControls => PlanView}/PlanView.qml (99%) rename src/{QmlControls => PlanView}/PlanViewRightPanel.qml (100%) rename src/{QmlControls => PlanView}/RallyPointEditorHeader.qml (100%) rename src/{QmlControls => PlanView}/RallyPointItemEditor.qml (100%) rename src/{QmlControls => PlanView}/RallyPointMapVisuals.qml (100%) rename src/{QmlControls => PlanView}/SimpleItemEditor.qml (100%) rename src/{QmlControls => PlanView}/SimpleItemMapVisual.qml (100%) rename src/{QmlControls => PlanView}/StructureScanEditor.qml (100%) rename src/{QmlControls => PlanView}/StructureScanMapVisual.qml (100%) rename src/{QmlControls => PlanView}/SurveyItemEditor.qml (100%) rename src/{QmlControls => PlanView}/SurveyMapVisual.qml (100%) rename src/{QmlControls => PlanView}/TakeoffItemMapVisual.qml (100%) rename src/{QmlControls => PlanView}/TerrainStatus.qml (100%) rename src/{QmlControls => PlanView}/TransectStyleComplexItemEditor.qml (100%) rename src/{QmlControls => PlanView}/TransectStyleComplexItemStats.qml (100%) rename src/{QmlControls => PlanView}/TransectStyleComplexItemTabBar.qml (100%) rename src/{QmlControls => PlanView}/TransectStyleComplexItemTerrainFollow.qml (100%) rename src/{QmlControls => PlanView}/TransectStyleMapVisuals.qml (100%) rename src/{QmlControls => PlanView}/TransformEditor.qml (100%) rename src/{QmlControls => PlanView}/VTOLLandingPatternEditor.qml (100%) rename src/{QmlControls => PlanView}/VTOLLandingPatternMapVisual.qml (100%) rename src/{QmlControls => UI/toolbar}/BatteryIndicator.qml (100%) rename src/{QmlControls => UI/toolbar}/FlightModeIndicator.qml (100%) rename src/{QmlControls => UI/toolbar}/FlightModeMenuIndicator.qml (100%) rename src/{QmlControls => UI/toolbar}/FlyViewToolBar.qml (100%) rename src/{QmlControls => UI/toolbar}/FlyViewToolBarIndicators.qml (100%) rename src/{QmlControls => UI/toolbar}/GPSIndicator.qml (100%) rename src/{QmlControls => UI/toolbar}/GPSIndicatorPage.qml (100%) rename src/{QmlControls => UI/toolbar}/MainStatusIndicator.qml (100%) rename src/{QmlControls => UI/toolbar}/MainStatusIndicatorOfflinePage.qml (100%) rename src/{QmlControls => UI/toolbar}/ParameterDownloadProgress.qml (100%) rename src/{QmlControls => UI/toolbar}/PlanViewToolBar.qml (99%) rename src/{QmlControls => UI/toolbar}/RemoteIDIndicatorPage.qml (100%) rename src/{QmlControls => UI/toolbar}/SignalStrength.qml (100%) rename src/{QmlControls => UI/toolbar}/VehicleMessageList.qml (100%) diff --git a/src/AutoPilotPlugins/APM/APMFollowComponent.qml b/src/AutoPilotPlugins/APM/APMFollowComponent.qml index 346e2dbcc939..24b8a262a164 100644 --- a/src/AutoPilotPlugins/APM/APMFollowComponent.qml +++ b/src/AutoPilotPlugins/APM/APMFollowComponent.qml @@ -6,6 +6,7 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls +import QGroundControl.PlanView SetupPage { id: followPage diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9c73a206fa69..6a6bd52091de 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,6 +45,7 @@ add_subdirectory(Utilities) add_subdirectory(Joystick) add_subdirectory(MAVLink) add_subdirectory(MissionManager) +add_subdirectory(PlanView) add_subdirectory(PositionManager) add_subdirectory(QmlControls) add_subdirectory(Settings) @@ -130,6 +131,7 @@ target_link_libraries(${CMAKE_PROJECT_NAME} FirstRunPromptDialogsModule FlyViewModule FlightMapModule + PlanViewModule QGroundControlControlsModule QGroundControlModule ToolbarModule diff --git a/src/FlightMap/MapItems/MissionItemIndicator.qml b/src/FlightMap/MapItems/MissionItemIndicator.qml index 8f1e3efddd08..76e53fa1220d 100644 --- a/src/FlightMap/MapItems/MissionItemIndicator.qml +++ b/src/FlightMap/MapItems/MissionItemIndicator.qml @@ -3,6 +3,7 @@ import QtLocation import QGroundControl import QGroundControl.Controls +import QGroundControl.PlanView /// Marker for displaying a mission item on the map MapQuickItem { diff --git a/src/FlightMap/MapItems/PlanMapItems.qml b/src/FlightMap/MapItems/PlanMapItems.qml index 329312cd9aad..8a37f7030ca3 100644 --- a/src/FlightMap/MapItems/PlanMapItems.qml +++ b/src/FlightMap/MapItems/PlanMapItems.qml @@ -5,6 +5,7 @@ import QtPositioning import QGroundControl import QGroundControl.Controls import QGroundControl.FlightMap +import QGroundControl.PlanView // Adds visual items associated with the Flight Plan to the map. // Currently only used by Fly View even though it's called PlanMapItems! diff --git a/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml b/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml index c54b9c540831..8888a72181de 100644 --- a/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml +++ b/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml @@ -8,6 +8,7 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls import QGroundControl.FlightMap +import QGroundControl.PlanView /// QGCMapPolygon map visuals Item { diff --git a/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml b/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml index bd11283becdc..43e2df894be1 100644 --- a/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml +++ b/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml @@ -7,6 +7,7 @@ import QtQuick.Dialogs import QGroundControl import QGroundControl.Controls import QGroundControl.FlightMap +import QGroundControl.PlanView /// QGCMapPolyline map visuals Item { diff --git a/src/FlyView/FlyView.qml b/src/FlyView/FlyView.qml index a58896c9d590..b617922cfd90 100644 --- a/src/FlyView/FlyView.qml +++ b/src/FlyView/FlyView.qml @@ -12,6 +12,7 @@ import QGroundControl import QGroundControl.Controls import QGroundControl.FlyView import QGroundControl.FlightMap +import QGroundControl.Toolbar import QGroundControl.Viewer3D Item { diff --git a/src/FlyView/FlyViewMap.qml b/src/FlyView/FlyViewMap.qml index 5b9dbf47b613..765dfb746035 100644 --- a/src/FlyView/FlyViewMap.qml +++ b/src/FlyView/FlyViewMap.qml @@ -9,6 +9,7 @@ import QGroundControl import QGroundControl.Controls import QGroundControl.FlyView import QGroundControl.FlightMap +import QGroundControl.PlanView FlightMap { id: _root diff --git a/src/MissionManager/CorridorScanComplexItem.cc b/src/MissionManager/CorridorScanComplexItem.cc index 7c13502a6bba..91c748cb09d5 100644 --- a/src/MissionManager/CorridorScanComplexItem.cc +++ b/src/MissionManager/CorridorScanComplexItem.cc @@ -18,7 +18,7 @@ CorridorScanComplexItem::CorridorScanComplexItem(PlanMasterController* masterCon , _metaDataMap (FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/CorridorScan.SettingsGroup.json"), this)) , _corridorWidthFact (settingsGroup, _metaDataMap[corridorWidthName]) { - _editorQml = "qrc:/qml/QGroundControl/Controls/CorridorScanEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/CorridorScanEditor.qml"; // We override the altitude to the mission default if (_cameraCalc.isManualCamera() || !_cameraCalc.valueSetIsDistance()->rawValue().toBool()) { diff --git a/src/MissionManager/FixedWingLandingComplexItem.cc b/src/MissionManager/FixedWingLandingComplexItem.cc index f65cf7ecb7c9..58c2c325463a 100644 --- a/src/MissionManager/FixedWingLandingComplexItem.cc +++ b/src/MissionManager/FixedWingLandingComplexItem.cc @@ -29,7 +29,7 @@ FixedWingLandingComplexItem::FixedWingLandingComplexItem(PlanMasterController* m , _stopTakingVideoFact (settingsGroup, _metaDataMap[stopTakingVideoName]) , _valueSetIsDistanceFact (settingsGroup, _metaDataMap[valueSetIsDistanceName]) { - _editorQml = "qrc:/qml/QGroundControl/Controls/FWLandingPatternEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/FWLandingPatternEditor.qml"; _isIncomplete = false; _init(); diff --git a/src/MissionManager/MissionSettingsItem.cc b/src/MissionManager/MissionSettingsItem.cc index 7aa007a40ad9..fb97888e5f08 100644 --- a/src/MissionManager/MissionSettingsItem.cc +++ b/src/MissionManager/MissionSettingsItem.cc @@ -19,7 +19,7 @@ MissionSettingsItem::MissionSettingsItem(PlanMasterController* masterController, , _speedSection (masterController) { _isIncomplete = false; - _editorQml = "qrc:/qml/QGroundControl/Controls/MissionSettingsEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/MissionSettingsEditor.qml"; if (_metaDataMap.isEmpty()) { _metaDataMap = FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/MissionSettings.FactMetaData.json"), nullptr /* metaDataParent */); diff --git a/src/MissionManager/SimpleMissionItem.cc b/src/MissionManager/SimpleMissionItem.cc index b79536ad8c14..8f886c6d52bf 100644 --- a/src/MissionManager/SimpleMissionItem.cc +++ b/src/MissionManager/SimpleMissionItem.cc @@ -36,7 +36,7 @@ SimpleMissionItem::SimpleMissionItem(PlanMasterController* masterController, boo , _param6MetaData (FactMetaData::valueTypeDouble) , _param7MetaData (FactMetaData::valueTypeDouble) { - _editorQml = QStringLiteral("qrc:/qml/QGroundControl/Controls/SimpleItemEditor.qml"); + _editorQml = QStringLiteral("qrc:/qml/QGroundControl/PlanView/SimpleItemEditor.qml"); _setupMetaData(); @@ -64,7 +64,7 @@ SimpleMissionItem::SimpleMissionItem(PlanMasterController* masterController, boo , _param6MetaData (FactMetaData::valueTypeDouble) , _param7MetaData (FactMetaData::valueTypeDouble) { - _editorQml = QStringLiteral("qrc:/qml/QGroundControl/Controls/SimpleItemEditor.qml"); + _editorQml = QStringLiteral("qrc:/qml/QGroundControl/PlanView/SimpleItemEditor.qml"); struct MavFrame2AltMode_s { MAV_FRAME mavFrame; diff --git a/src/MissionManager/StructureScanComplexItem.cc b/src/MissionManager/StructureScanComplexItem.cc index dd1955bcd9e2..25d547219b53 100644 --- a/src/MissionManager/StructureScanComplexItem.cc +++ b/src/MissionManager/StructureScanComplexItem.cc @@ -31,7 +31,7 @@ StructureScanComplexItem::StructureScanComplexItem(PlanMasterController* masterC , _startFromTopFact (settingsGroup, _metaDataMap[startFromTopName]) , _entranceAltFact (settingsGroup, _metaDataMap[_entranceAltName]) { - _editorQml = "qrc:/qml/QGroundControl/Controls/StructureScanEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/StructureScanEditor.qml"; _entranceAltFact.setRawValue(SettingsManager::instance()->appSettings()->defaultMissionItemAltitude()->rawValue()); diff --git a/src/MissionManager/SurveyComplexItem.cc b/src/MissionManager/SurveyComplexItem.cc index 095035616ee7..3868c378befe 100644 --- a/src/MissionManager/SurveyComplexItem.cc +++ b/src/MissionManager/SurveyComplexItem.cc @@ -26,7 +26,7 @@ SurveyComplexItem::SurveyComplexItem(PlanMasterController* masterController, boo , _splitConcavePolygonsFact (settingsGroup, _metaDataMap[splitConcavePolygonsName]) , _entryPoint (EntryLocationTopLeft) { - _editorQml = "qrc:/qml/QGroundControl/Controls/SurveyItemEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/SurveyItemEditor.qml"; if (_controllerVehicle && !(_controllerVehicle->fixedWing() || _controllerVehicle->vtol())) { // Only fixed wing flight paths support alternate transects diff --git a/src/MissionManager/TakeoffMissionItem.cc b/src/MissionManager/TakeoffMissionItem.cc index f2e9d2dd1635..19248ddad33a 100644 --- a/src/MissionManager/TakeoffMissionItem.cc +++ b/src/MissionManager/TakeoffMissionItem.cc @@ -37,7 +37,7 @@ TakeoffMissionItem::~TakeoffMissionItem() void TakeoffMissionItem::_init(bool forLoad) { - _editorQml = QStringLiteral("qrc:/qml/QGroundControl/Controls/SimpleItemEditor.qml"); + _editorQml = QStringLiteral("qrc:/qml/QGroundControl/PlanView/SimpleItemEditor.qml"); connect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &TakeoffMissionItem::launchCoordinateChanged); diff --git a/src/MissionManager/VTOLLandingComplexItem.cc b/src/MissionManager/VTOLLandingComplexItem.cc index b99b3e5b0c1c..b6fdd084e825 100644 --- a/src/MissionManager/VTOLLandingComplexItem.cc +++ b/src/MissionManager/VTOLLandingComplexItem.cc @@ -30,7 +30,7 @@ VTOLLandingComplexItem::VTOLLandingComplexItem(PlanMasterController* masterContr , _stopTakingPhotosFact (settingsGroup, _metaDataMap[stopTakingPhotosName]) , _stopTakingVideoFact (settingsGroup, _metaDataMap[stopTakingVideoName]) { - _editorQml = "qrc:/qml/QGroundControl/Controls/VTOLLandingPatternEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/VTOLLandingPatternEditor.qml"; _isIncomplete = false; _init(); diff --git a/src/PlanView/CMakeLists.txt b/src/PlanView/CMakeLists.txt new file mode 100644 index 000000000000..1ba911a8d1d0 --- /dev/null +++ b/src/PlanView/CMakeLists.txt @@ -0,0 +1,53 @@ +# ============================================================================ +# Plan View Module +# Mission planning view, editors, and map visuals +# ============================================================================ + +qt_add_library(PlanViewModule STATIC) + +qt_add_qml_module(PlanViewModule + URI QGroundControl.PlanView + VERSION 1.0 + RESOURCE_PREFIX /qml + QML_FILES + CameraCalcCamera.qml + CameraCalcGrid.qml + CameraSection.qml + CorridorScanEditor.qml + CorridorScanMapVisual.qml + FWLandingPatternEditor.qml + FWLandingPatternMapVisual.qml + GeoFenceEditor.qml + GeoFenceMapVisuals.qml + MissionItemEditor.qml + MissionItemIndexLabel.qml + MissionItemMapVisual.qml + MissionItemStatus.qml + MissionItemTreeView.qml + MissionSettingsEditor.qml + MissionStats.qml + PlanEditToolbar.qml + PlanToolBarIndicators.qml + PlanView.qml + PlanViewRightPanel.qml + RallyPointEditorHeader.qml + RallyPointItemEditor.qml + RallyPointMapVisuals.qml + SimpleItemEditor.qml + SimpleItemMapVisual.qml + StructureScanEditor.qml + StructureScanMapVisual.qml + SurveyItemEditor.qml + SurveyMapVisual.qml + TakeoffItemMapVisual.qml + TerrainStatus.qml + TransectStyleComplexItemEditor.qml + TransectStyleComplexItemStats.qml + TransectStyleComplexItemTabBar.qml + TransectStyleComplexItemTerrainFollow.qml + TransectStyleMapVisuals.qml + TransformEditor.qml + VTOLLandingPatternEditor.qml + VTOLLandingPatternMapVisual.qml + NO_PLUGIN +) diff --git a/src/QmlControls/CameraCalcCamera.qml b/src/PlanView/CameraCalcCamera.qml similarity index 100% rename from src/QmlControls/CameraCalcCamera.qml rename to src/PlanView/CameraCalcCamera.qml diff --git a/src/QmlControls/CameraCalcGrid.qml b/src/PlanView/CameraCalcGrid.qml similarity index 100% rename from src/QmlControls/CameraCalcGrid.qml rename to src/PlanView/CameraCalcGrid.qml diff --git a/src/QmlControls/CameraSection.qml b/src/PlanView/CameraSection.qml similarity index 100% rename from src/QmlControls/CameraSection.qml rename to src/PlanView/CameraSection.qml diff --git a/src/QmlControls/CorridorScanEditor.qml b/src/PlanView/CorridorScanEditor.qml similarity index 100% rename from src/QmlControls/CorridorScanEditor.qml rename to src/PlanView/CorridorScanEditor.qml diff --git a/src/QmlControls/CorridorScanMapVisual.qml b/src/PlanView/CorridorScanMapVisual.qml similarity index 100% rename from src/QmlControls/CorridorScanMapVisual.qml rename to src/PlanView/CorridorScanMapVisual.qml diff --git a/src/QmlControls/FWLandingPatternEditor.qml b/src/PlanView/FWLandingPatternEditor.qml similarity index 100% rename from src/QmlControls/FWLandingPatternEditor.qml rename to src/PlanView/FWLandingPatternEditor.qml diff --git a/src/QmlControls/FWLandingPatternMapVisual.qml b/src/PlanView/FWLandingPatternMapVisual.qml similarity index 100% rename from src/QmlControls/FWLandingPatternMapVisual.qml rename to src/PlanView/FWLandingPatternMapVisual.qml diff --git a/src/QmlControls/GeoFenceEditor.qml b/src/PlanView/GeoFenceEditor.qml similarity index 100% rename from src/QmlControls/GeoFenceEditor.qml rename to src/PlanView/GeoFenceEditor.qml diff --git a/src/QmlControls/GeoFenceMapVisuals.qml b/src/PlanView/GeoFenceMapVisuals.qml similarity index 100% rename from src/QmlControls/GeoFenceMapVisuals.qml rename to src/PlanView/GeoFenceMapVisuals.qml diff --git a/src/QmlControls/MissionItemEditor.qml b/src/PlanView/MissionItemEditor.qml similarity index 100% rename from src/QmlControls/MissionItemEditor.qml rename to src/PlanView/MissionItemEditor.qml diff --git a/src/QmlControls/MissionItemIndexLabel.qml b/src/PlanView/MissionItemIndexLabel.qml similarity index 100% rename from src/QmlControls/MissionItemIndexLabel.qml rename to src/PlanView/MissionItemIndexLabel.qml diff --git a/src/QmlControls/MissionItemMapVisual.qml b/src/PlanView/MissionItemMapVisual.qml similarity index 100% rename from src/QmlControls/MissionItemMapVisual.qml rename to src/PlanView/MissionItemMapVisual.qml diff --git a/src/QmlControls/MissionItemStatus.qml b/src/PlanView/MissionItemStatus.qml similarity index 100% rename from src/QmlControls/MissionItemStatus.qml rename to src/PlanView/MissionItemStatus.qml diff --git a/src/QmlControls/MissionItemTreeView.qml b/src/PlanView/MissionItemTreeView.qml similarity index 99% rename from src/QmlControls/MissionItemTreeView.qml rename to src/PlanView/MissionItemTreeView.qml index 04e3746e0362..66eea356566d 100644 --- a/src/QmlControls/MissionItemTreeView.qml +++ b/src/PlanView/MissionItemTreeView.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls import QGroundControl.FactControls +import QGroundControl.PlanView /// Unified plan tree view showing Mission Items, GeoFence, and Rally Points /// as collapsible sections using a real TreeView with type-discriminating delegates. diff --git a/src/QmlControls/MissionSettingsEditor.qml b/src/PlanView/MissionSettingsEditor.qml similarity index 100% rename from src/QmlControls/MissionSettingsEditor.qml rename to src/PlanView/MissionSettingsEditor.qml diff --git a/src/QmlControls/MissionStats.qml b/src/PlanView/MissionStats.qml similarity index 100% rename from src/QmlControls/MissionStats.qml rename to src/PlanView/MissionStats.qml diff --git a/src/QmlControls/PlanEditToolbar.qml b/src/PlanView/PlanEditToolbar.qml similarity index 100% rename from src/QmlControls/PlanEditToolbar.qml rename to src/PlanView/PlanEditToolbar.qml diff --git a/src/QmlControls/PlanToolBarIndicators.qml b/src/PlanView/PlanToolBarIndicators.qml similarity index 100% rename from src/QmlControls/PlanToolBarIndicators.qml rename to src/PlanView/PlanToolBarIndicators.qml diff --git a/src/QmlControls/PlanView.qml b/src/PlanView/PlanView.qml similarity index 99% rename from src/QmlControls/PlanView.qml rename to src/PlanView/PlanView.qml index f1bbcee23251..45e07d2e44e9 100644 --- a/src/QmlControls/PlanView.qml +++ b/src/PlanView/PlanView.qml @@ -11,6 +11,7 @@ import QGroundControl.FlightMap import QGroundControl.Controls import QGroundControl.FactControls import QGroundControl.FlyView +import QGroundControl.Toolbar Item { id: _root diff --git a/src/QmlControls/PlanViewRightPanel.qml b/src/PlanView/PlanViewRightPanel.qml similarity index 100% rename from src/QmlControls/PlanViewRightPanel.qml rename to src/PlanView/PlanViewRightPanel.qml diff --git a/src/QmlControls/RallyPointEditorHeader.qml b/src/PlanView/RallyPointEditorHeader.qml similarity index 100% rename from src/QmlControls/RallyPointEditorHeader.qml rename to src/PlanView/RallyPointEditorHeader.qml diff --git a/src/QmlControls/RallyPointItemEditor.qml b/src/PlanView/RallyPointItemEditor.qml similarity index 100% rename from src/QmlControls/RallyPointItemEditor.qml rename to src/PlanView/RallyPointItemEditor.qml diff --git a/src/QmlControls/RallyPointMapVisuals.qml b/src/PlanView/RallyPointMapVisuals.qml similarity index 100% rename from src/QmlControls/RallyPointMapVisuals.qml rename to src/PlanView/RallyPointMapVisuals.qml diff --git a/src/QmlControls/SimpleItemEditor.qml b/src/PlanView/SimpleItemEditor.qml similarity index 100% rename from src/QmlControls/SimpleItemEditor.qml rename to src/PlanView/SimpleItemEditor.qml diff --git a/src/QmlControls/SimpleItemMapVisual.qml b/src/PlanView/SimpleItemMapVisual.qml similarity index 100% rename from src/QmlControls/SimpleItemMapVisual.qml rename to src/PlanView/SimpleItemMapVisual.qml diff --git a/src/QmlControls/StructureScanEditor.qml b/src/PlanView/StructureScanEditor.qml similarity index 100% rename from src/QmlControls/StructureScanEditor.qml rename to src/PlanView/StructureScanEditor.qml diff --git a/src/QmlControls/StructureScanMapVisual.qml b/src/PlanView/StructureScanMapVisual.qml similarity index 100% rename from src/QmlControls/StructureScanMapVisual.qml rename to src/PlanView/StructureScanMapVisual.qml diff --git a/src/QmlControls/SurveyItemEditor.qml b/src/PlanView/SurveyItemEditor.qml similarity index 100% rename from src/QmlControls/SurveyItemEditor.qml rename to src/PlanView/SurveyItemEditor.qml diff --git a/src/QmlControls/SurveyMapVisual.qml b/src/PlanView/SurveyMapVisual.qml similarity index 100% rename from src/QmlControls/SurveyMapVisual.qml rename to src/PlanView/SurveyMapVisual.qml diff --git a/src/QmlControls/TakeoffItemMapVisual.qml b/src/PlanView/TakeoffItemMapVisual.qml similarity index 100% rename from src/QmlControls/TakeoffItemMapVisual.qml rename to src/PlanView/TakeoffItemMapVisual.qml diff --git a/src/QmlControls/TerrainStatus.qml b/src/PlanView/TerrainStatus.qml similarity index 100% rename from src/QmlControls/TerrainStatus.qml rename to src/PlanView/TerrainStatus.qml diff --git a/src/QmlControls/TransectStyleComplexItemEditor.qml b/src/PlanView/TransectStyleComplexItemEditor.qml similarity index 100% rename from src/QmlControls/TransectStyleComplexItemEditor.qml rename to src/PlanView/TransectStyleComplexItemEditor.qml diff --git a/src/QmlControls/TransectStyleComplexItemStats.qml b/src/PlanView/TransectStyleComplexItemStats.qml similarity index 100% rename from src/QmlControls/TransectStyleComplexItemStats.qml rename to src/PlanView/TransectStyleComplexItemStats.qml diff --git a/src/QmlControls/TransectStyleComplexItemTabBar.qml b/src/PlanView/TransectStyleComplexItemTabBar.qml similarity index 100% rename from src/QmlControls/TransectStyleComplexItemTabBar.qml rename to src/PlanView/TransectStyleComplexItemTabBar.qml diff --git a/src/QmlControls/TransectStyleComplexItemTerrainFollow.qml b/src/PlanView/TransectStyleComplexItemTerrainFollow.qml similarity index 100% rename from src/QmlControls/TransectStyleComplexItemTerrainFollow.qml rename to src/PlanView/TransectStyleComplexItemTerrainFollow.qml diff --git a/src/QmlControls/TransectStyleMapVisuals.qml b/src/PlanView/TransectStyleMapVisuals.qml similarity index 100% rename from src/QmlControls/TransectStyleMapVisuals.qml rename to src/PlanView/TransectStyleMapVisuals.qml diff --git a/src/QmlControls/TransformEditor.qml b/src/PlanView/TransformEditor.qml similarity index 100% rename from src/QmlControls/TransformEditor.qml rename to src/PlanView/TransformEditor.qml diff --git a/src/QmlControls/VTOLLandingPatternEditor.qml b/src/PlanView/VTOLLandingPatternEditor.qml similarity index 100% rename from src/QmlControls/VTOLLandingPatternEditor.qml rename to src/PlanView/VTOLLandingPatternEditor.qml diff --git a/src/QmlControls/VTOLLandingPatternMapVisual.qml b/src/PlanView/VTOLLandingPatternMapVisual.qml similarity index 100% rename from src/QmlControls/VTOLLandingPatternMapVisual.qml rename to src/PlanView/VTOLLandingPatternMapVisual.qml diff --git a/src/QmlControls/CMakeLists.txt b/src/QmlControls/CMakeLists.txt index 9d5dde97e8e9..2265877a8962 100644 --- a/src/QmlControls/CMakeLists.txt +++ b/src/QmlControls/CMakeLists.txt @@ -117,7 +117,6 @@ qt_add_qml_module(QGroundControlControlsModule MAVLinkChart.qml MAVLinkMessageButton.qml MissionCommandDialog.qml - MissionItemIndexLabel.qml MvPanelPage.qml OfflineMapButton.qml ParameterDiffDialog.qml @@ -178,14 +177,15 @@ qt_add_qml_module(QGroundControlControlsModule ToolStrip.qml ToolStripDropPanel.qml ToolStripHoverButton.qml - TransformEditor.qml ValueSlider.qml VehicleRotationCal.qml VehicleSummaryRow.qml -# NOTE: Due to Windows limitations with set_source_files_properties and qt_add_qml_module, -# various QML files from other modules are temporarily included here. -# This will be revisited in a future release. +# NOTE: The QML files below belong to other modules but are declared here because +# CMake's set_source_files_properties() has directory scope. Properties like +# QT_QML_SINGLETON_TYPE must be visible in the same CMakeLists.txt that calls +# qt_add_qml_module(). On Windows this cross-directory property propagation fails, +# so these files are kept in this module until a better solution is available. # src/AnalyzeView/ AnalyzePage.qml @@ -193,64 +193,9 @@ qt_add_qml_module(QGroundControlControlsModule # src/FirstRunPromptDialogs/ FirstRunPrompt.qml - # src/PlanView/ - CameraCalcCamera.qml - CameraCalcGrid.qml - CameraSection.qml - CorridorScanEditor.qml - CorridorScanMapVisual.qml - FWLandingPatternEditor.qml - FWLandingPatternMapVisual.qml - GeoFenceEditor.qml - GeoFenceMapVisuals.qml - MissionItemEditor.qml - MissionItemMapVisual.qml - MissionItemStatus.qml - MissionItemTreeView.qml - MissionSettingsEditor.qml - MissionStats.qml - PlanEditToolbar.qml - PlanToolBarIndicators.qml - PlanView.qml - PlanViewRightPanel.qml - RallyPointEditorHeader.qml - RallyPointItemEditor.qml - RallyPointMapVisuals.qml - SimpleItemEditor.qml - SimpleItemMapVisual.qml - StructureScanEditor.qml - StructureScanMapVisual.qml - SurveyItemEditor.qml - SurveyMapVisual.qml - TakeoffItemMapVisual.qml - TerrainStatus.qml - TransectStyleComplexItemEditor.qml - TransectStyleComplexItemStats.qml - TransectStyleComplexItemTabBar.qml - TransectStyleComplexItemTerrainFollow.qml - TransectStyleMapVisuals.qml - VTOLLandingPatternEditor.qml - VTOLLandingPatternMapVisual.qml - # src/UI/preferences/ OfflineMapInfo.qml - # src/UI/toolbar/ - BatteryIndicator.qml - FlightModeIndicator.qml - FlightModeMenuIndicator.qml - FlyViewToolBar.qml - FlyViewToolBarIndicators.qml - GPSIndicator.qml - GPSIndicatorPage.qml - MainStatusIndicator.qml - MainStatusIndicatorOfflinePage.qml - ParameterDownloadProgress.qml - PlanViewToolBar.qml - RemoteIDIndicatorPage.qml - SignalStrength.qml - VehicleMessageList.qml - # src/QtLocationPlugin/QMLControl/ OfflineMapEditor.qml diff --git a/src/UI/MainWindow.qml b/src/UI/MainWindow.qml index 4278b780ed9a..7c10f4c84262 100644 --- a/src/UI/MainWindow.qml +++ b/src/UI/MainWindow.qml @@ -9,6 +9,7 @@ import QGroundControl.Controls import QGroundControl.FactControls import QGroundControl.FlyView import QGroundControl.FlightMap +import QGroundControl.PlanView import QGroundControl.Toolbar /// @brief Native QML top level window diff --git a/src/QmlControls/BatteryIndicator.qml b/src/UI/toolbar/BatteryIndicator.qml similarity index 100% rename from src/QmlControls/BatteryIndicator.qml rename to src/UI/toolbar/BatteryIndicator.qml diff --git a/src/UI/toolbar/CMakeLists.txt b/src/UI/toolbar/CMakeLists.txt index 547d24baecca..7fe6ab1d913e 100644 --- a/src/UI/toolbar/CMakeLists.txt +++ b/src/UI/toolbar/CMakeLists.txt @@ -6,19 +6,33 @@ qt_add_qml_module(ToolbarModule RESOURCE_PREFIX /qml QML_FILES ArmedIndicator.qml + BatteryIndicator.qml EscIndicator.qml EscIndicatorPage.qml + FlightModeIndicator.qml + FlightModeMenuIndicator.qml + FlyViewToolBar.qml + FlyViewToolBarIndicators.qml GCSControlIndicator.qml GimbalIndicator.qml + GPSIndicator.qml + GPSIndicatorPage.qml GPSResilienceIndicator.qml JoystickIndicator.qml + MainStatusIndicator.qml + MainStatusIndicatorOfflinePage.qml ModeIndicator.qml MultiVehicleSelector.qml + ParameterDownloadProgress.qml + PlanViewToolBar.qml RCRSSIIndicator.qml RemoteIDIndicator.qml + RemoteIDIndicatorPage.qml RTKGPSIndicator.qml SelectViewDropdown.qml + SignalStrength.qml TelemetryRSSIIndicator.qml VehicleGPSIndicator.qml + VehicleMessageList.qml NO_PLUGIN ) diff --git a/src/QmlControls/FlightModeIndicator.qml b/src/UI/toolbar/FlightModeIndicator.qml similarity index 100% rename from src/QmlControls/FlightModeIndicator.qml rename to src/UI/toolbar/FlightModeIndicator.qml diff --git a/src/QmlControls/FlightModeMenuIndicator.qml b/src/UI/toolbar/FlightModeMenuIndicator.qml similarity index 100% rename from src/QmlControls/FlightModeMenuIndicator.qml rename to src/UI/toolbar/FlightModeMenuIndicator.qml diff --git a/src/QmlControls/FlyViewToolBar.qml b/src/UI/toolbar/FlyViewToolBar.qml similarity index 100% rename from src/QmlControls/FlyViewToolBar.qml rename to src/UI/toolbar/FlyViewToolBar.qml diff --git a/src/QmlControls/FlyViewToolBarIndicators.qml b/src/UI/toolbar/FlyViewToolBarIndicators.qml similarity index 100% rename from src/QmlControls/FlyViewToolBarIndicators.qml rename to src/UI/toolbar/FlyViewToolBarIndicators.qml diff --git a/src/QmlControls/GPSIndicator.qml b/src/UI/toolbar/GPSIndicator.qml similarity index 100% rename from src/QmlControls/GPSIndicator.qml rename to src/UI/toolbar/GPSIndicator.qml diff --git a/src/QmlControls/GPSIndicatorPage.qml b/src/UI/toolbar/GPSIndicatorPage.qml similarity index 100% rename from src/QmlControls/GPSIndicatorPage.qml rename to src/UI/toolbar/GPSIndicatorPage.qml diff --git a/src/QmlControls/MainStatusIndicator.qml b/src/UI/toolbar/MainStatusIndicator.qml similarity index 100% rename from src/QmlControls/MainStatusIndicator.qml rename to src/UI/toolbar/MainStatusIndicator.qml diff --git a/src/QmlControls/MainStatusIndicatorOfflinePage.qml b/src/UI/toolbar/MainStatusIndicatorOfflinePage.qml similarity index 100% rename from src/QmlControls/MainStatusIndicatorOfflinePage.qml rename to src/UI/toolbar/MainStatusIndicatorOfflinePage.qml diff --git a/src/QmlControls/ParameterDownloadProgress.qml b/src/UI/toolbar/ParameterDownloadProgress.qml similarity index 100% rename from src/QmlControls/ParameterDownloadProgress.qml rename to src/UI/toolbar/ParameterDownloadProgress.qml diff --git a/src/QmlControls/PlanViewToolBar.qml b/src/UI/toolbar/PlanViewToolBar.qml similarity index 99% rename from src/QmlControls/PlanViewToolBar.qml rename to src/UI/toolbar/PlanViewToolBar.qml index 57095c2d2f5c..62013c92346a 100644 --- a/src/QmlControls/PlanViewToolBar.qml +++ b/src/UI/toolbar/PlanViewToolBar.qml @@ -5,6 +5,7 @@ import QtQuick.Dialogs import QGroundControl import QGroundControl.Controls +import QGroundControl.PlanView Rectangle { id: _root diff --git a/src/QmlControls/RemoteIDIndicatorPage.qml b/src/UI/toolbar/RemoteIDIndicatorPage.qml similarity index 100% rename from src/QmlControls/RemoteIDIndicatorPage.qml rename to src/UI/toolbar/RemoteIDIndicatorPage.qml diff --git a/src/QmlControls/SignalStrength.qml b/src/UI/toolbar/SignalStrength.qml similarity index 100% rename from src/QmlControls/SignalStrength.qml rename to src/UI/toolbar/SignalStrength.qml diff --git a/src/QmlControls/VehicleMessageList.qml b/src/UI/toolbar/VehicleMessageList.qml similarity index 100% rename from src/QmlControls/VehicleMessageList.qml rename to src/UI/toolbar/VehicleMessageList.qml From a0a77abebbb82a9dff0d93f1634116534c1e0601 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 8 Mar 2026 10:11:47 -0700 Subject: [PATCH 053/119] Clean up unit test artifacts on shutdown Remove settings file, app-specific settings directory, and app data directory after unit test and simple boot test runs. Use ParameterManager::parameterCacheDir() to derive the settings directory instead of duplicating the path construction logic. Redirect test save path to the system temp directory so test artifacts don't pollute the user's Documents folder. --- src/QGCApplication.cc | 32 +++++++++++++++++++ src/Settings/AppSettings.cc | 7 +++- .../Bluetooth/BluetoothConfigurationTest.cc | 4 +++ .../Bluetooth/BluetoothLiveAdapterTest.cc | 4 +++ test/Comms/Bluetooth/BluetoothWorkerTest.cc | 4 +++ test/MissionManager/MissionControllerTest.cc | 11 ++++--- 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/QGCApplication.cc b/src/QGCApplication.cc index b2f8da65b9f3..bb6381a3e33a 100644 --- a/src/QGCApplication.cc +++ b/src/QGCApplication.cc @@ -677,6 +677,38 @@ void QGCApplication::shutdown() QGCCorePlugin::instance()->cleanup(); + if (_runningUnitTests || _simpleBootTest) { + const QSettings settings; + const QString settingsFile = settings.fileName(); + if (QFile::exists(settingsFile)) { + if (QFile::remove(settingsFile)) { + qCDebug(QGCApplicationLog) << "Removed test run settings file:" << settingsFile; + } else { + qCWarning(QGCApplicationLog) << "Failed to remove test run settings file:" << settingsFile; + } + } + + // Remove the app-specific settings directory (parent of ParamCache) + QDir settingsAppDir(ParameterManager::parameterCacheDir()); + settingsAppDir.cdUp(); + if (settingsAppDir.exists()) { + if (settingsAppDir.removeRecursively()) { + qCDebug(QGCApplicationLog) << "Removed test run settings directory:" << settingsAppDir.absolutePath(); + } else { + qCWarning(QGCApplicationLog) << "Failed to remove test run settings directory:" << settingsAppDir.absolutePath(); + } + } + + QDir appDir(SettingsManager::instance()->appSettings()->savePath()->rawValue().toString()); + if (appDir.exists()) { + if (appDir.removeRecursively()) { + qCDebug(QGCApplicationLog) << "Removed test run app data directory:" << appDir.absolutePath(); + } else { + qCWarning(QGCApplicationLog) << "Failed to remove test run app data directory:" << appDir.absolutePath(); + } + } + } + // This is bad, but currently qobject inheritances are incorrect and cause crashes on exit without delete _qmlAppEngine; } diff --git a/src/Settings/AppSettings.cc b/src/Settings/AppSettings.cc index c7a7a309dc2b..1c511926d6bb 100644 --- a/src/Settings/AppSettings.cc +++ b/src/Settings/AppSettings.cc @@ -96,7 +96,12 @@ DECLARE_SETTINGGROUP(App, "") #endif savePathFact->setVisible(false); #else - QDir rootDir = QDir(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + QDir rootDir; + if (qgcApp()->runningUnitTests() || qgcApp()->simpleBootTest()) { + rootDir = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); + } else { + rootDir = QDir(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + } savePathFact->setRawValue(rootDir.filePath(appName)); #endif } diff --git a/test/Comms/Bluetooth/BluetoothConfigurationTest.cc b/test/Comms/Bluetooth/BluetoothConfigurationTest.cc index a46af70ba263..6904b9b5ce69 100644 --- a/test/Comms/Bluetooth/BluetoothConfigurationTest.cc +++ b/test/Comms/Bluetooth/BluetoothConfigurationTest.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -101,6 +102,9 @@ void BluetoothConfigurationTest::_testBleUuidGetSet() void BluetoothConfigurationTest::_testBluetoothAvailabilityConsistency() { + // Qt's Bluetooth module may emit an uncategorized warning when BlueZ is not running (e.g. on headless CI). + expectLogMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Cannot find a compatible running Bluez"))); + QCOMPARE(BluetoothConfiguration::isBluetoothAvailable(), LinkManager::isBluetoothAvailable()); } diff --git a/test/Comms/Bluetooth/BluetoothLiveAdapterTest.cc b/test/Comms/Bluetooth/BluetoothLiveAdapterTest.cc index 3a0335e00279..17cd35b6b340 100644 --- a/test/Comms/Bluetooth/BluetoothLiveAdapterTest.cc +++ b/test/Comms/Bluetooth/BluetoothLiveAdapterTest.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -45,6 +46,9 @@ static QString _firstAdapterAddressOrEmpty(const QVariantList &adapters) void BluetoothLiveAdapterTest::_testLiveAdapterSelectionAndState() { + // Qt's Bluetooth module may emit an uncategorized warning when BlueZ is not running (e.g. on headless CI). + expectLogMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Cannot find a compatible running Bluez"))); + const QList localHosts = _localAdaptersOrSkip(); if (localHosts.isEmpty()) { QSKIP("No local Bluetooth adapter detected on this host"); diff --git a/test/Comms/Bluetooth/BluetoothWorkerTest.cc b/test/Comms/Bluetooth/BluetoothWorkerTest.cc index e5d8c8f6dd1f..22126b4324e3 100644 --- a/test/Comms/Bluetooth/BluetoothWorkerTest.cc +++ b/test/Comms/Bluetooth/BluetoothWorkerTest.cc @@ -6,6 +6,7 @@ #include "BluetoothWorker.h" #include +#include #include #include @@ -22,6 +23,9 @@ static QTimer *_findServiceDiscoveryTimer(const BluetoothWorker *worker) void BluetoothWorkerTest::_testFactoryCreatesClassicWorker() { + // Qt's Bluetooth module may emit an uncategorized warning when BlueZ is not running (e.g. on headless CI). + expectLogMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Cannot find a compatible running Bluez"))); + BluetoothConfiguration config("TestBT_factoryClassic"); config.setMode(BluetoothConfiguration::BluetoothMode::ModeClassic); diff --git a/test/MissionManager/MissionControllerTest.cc b/test/MissionManager/MissionControllerTest.cc index 06bad0b6fb8e..08a4f59a5694 100644 --- a/test/MissionManager/MissionControllerTest.cc +++ b/test/MissionManager/MissionControllerTest.cc @@ -338,15 +338,16 @@ void MissionControllerTest::_testMissionTransformsInvalidHome() settingsItem->setCoordinate(QGeoCoordinate()); QVERIFY_TRUE_WAIT(!settingsItem->coordinate().isValid(), TestTimeout::shortMs()); + // repositionMission and rotateMission require a valid home — they should be no-ops _missionController->repositionMission(home.atDistanceAndAzimuth(100.0, 0.0), true, true); - _missionController->offsetMission(10.0, 5.0, 8.0, true, true); _missionController->rotateMission(45.0, true, true); - QVERIFY_TRUE_WAIT((item1->coordinate().distanceTo(oldItemCoord) <= kCoordToleranceMeters) && - (qAbs(item1->editableAlt() - oldItemAlt) <= kAltToleranceMeters), - TestTimeout::shortMs()); - QCOMPARE_COORDS(item1->coordinate(), oldItemCoord, kCoordToleranceMeters); QCOMPARE_FUZZY(item1->editableAlt(), oldItemAlt, kAltToleranceMeters); + + // offsetMission does not depend on home — it should still apply + _missionController->offsetMission(10.0, 5.0, 8.0, true, true); + QVERIFY(item1->coordinate().distanceTo(oldItemCoord) > kCoordToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldItemAlt + 8.0, kAltToleranceMeters); } void MissionControllerTest::_testLoadJsonSectionAvailable() From 31cbfc82f120a84beb28a150622bd38f144e3f33 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 8 Mar 2026 10:11:54 -0700 Subject: [PATCH 054/119] CI: run unit tests in Linux Debug build, disable broken jobs Add unit test execution and result reporting to the Linux Debug build job, replacing the dedicated debug-validation job which fails 100% of the time due to coverage and ASAN issues. Temporarily disable the debug-validation job (coverage + ASAN) in linux.yml and the Docker build job in docker.yml. Original conditions are preserved as comments for easy re-enablement. --- .github/workflows/docker.yml | 2 +- .github/workflows/linux.yml | 55 +++++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8609f85bed1e..17f616fb2d42 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -121,7 +121,7 @@ jobs: name: Docker ${{ matrix.platform }} runs-on: ubuntu-latest needs: [changes, plan-builds] - if: always() && !cancelled() && needs.plan-builds.outputs.has_jobs == 'true' + if: always() && !cancelled() && needs.plan-builds.outputs.has_jobs == 'true' && vars.DOCKER_BUILD_ENABLED == 'true' timeout-minutes: 120 strategy: diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 74ab664af963..350c3afd014f 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -128,6 +128,29 @@ jobs: with: category: "/language:c-cpp" + - name: Run Unit Tests + if: matrix.build_type == 'Debug' + uses: ./.github/actions/run-unit-tests + with: + build-dir: ${{ runner.temp }}/build + junit-output: junit-results-linux-${{ matrix.arch }}.xml + ctest-output: test-output-linux-${{ matrix.arch }}.txt + include-labels: 'Unit|Integration' + exclude-labels: 'Flaky|Network' + + - name: Report Test Results + if: always() && !cancelled() && matrix.build_type == 'Debug' + uses: ./.github/actions/test-report + with: + name: Unit Tests (${{ matrix.arch }}) + build-dir: ${{ runner.temp }}/build + junit-file: junit-results-linux-${{ matrix.arch }}.xml + output-file: test-output-linux-${{ matrix.arch }}.txt + artifact-name: test-results-${{ matrix.arch }} + retention-days: 7 + trunk-org-slug: ${{ vars.TRUNK_ORG_SLUG }} + trunk-token: ${{ secrets.TRUNK_TOKEN }} + - name: Verify Executable if: matrix.build_type == 'Release' uses: ./.github/actions/verify-executable @@ -190,11 +213,11 @@ jobs: fail-fast: false matrix: include: - - job_name: Test + Coverage linux_gcc_64 Debug - mode: coverage - timeout_minutes: 60 - fetch_depth: 0 - fetch_tags: false + # - job_name: Test + Coverage linux_gcc_64 Debug + # mode: coverage + # timeout_minutes: 60 + # fetch_depth: 0 + # fetch_tags: false - job_name: Sanitizers linux_gcc_64 Debug (ASan+UBSan) mode: sanitizers timeout_minutes: 120 @@ -218,7 +241,7 @@ jobs: fetch-tags: ${{ matrix.fetch_tags }} - name: Restore file timestamps - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: chetan/git-restore-mtime-action@v2 - name: Build Setup @@ -234,7 +257,7 @@ jobs: uses: ./.github/actions/install-dependencies - name: Configure (Coverage) - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/cmake-configure with: build-dir: ${{ runner.temp }}/build @@ -244,7 +267,7 @@ jobs: stable: ${{ (github.ref_type == 'tag' || contains(github.ref, 'Stable')) && 'true' || 'false' }} - name: Build (Coverage) - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/cmake-build with: build-dir: ${{ runner.temp }}/build @@ -268,7 +291,7 @@ jobs: build-type: Debug - name: Run Unit Tests - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/run-unit-tests with: build-dir: ${{ runner.temp }}/build @@ -296,7 +319,7 @@ jobs: parallel: '1' - name: Analyze Unit Test Durations - if: always() && !cancelled() && matrix.mode == 'coverage' + if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/test-duration-report with: junit-path: ${{ runner.temp }}/build/junit-results-linux-linux_gcc_64.xml @@ -322,7 +345,7 @@ jobs: fail-on-regression: 'false' - name: Report Test Results - if: always() && !cancelled() && matrix.mode == 'coverage' + if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/test-report with: name: Unit Tests (linux_gcc_64) @@ -335,7 +358,7 @@ jobs: trunk-token: ${{ secrets.TRUNK_TOKEN }} - name: Upload Duration Report - if: always() && !cancelled() && matrix.mode == 'coverage' + if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: actions/upload-artifact@v6 with: name: test-duration-linux_gcc_64 @@ -354,7 +377,7 @@ jobs: retention-days: 7 - name: Ensure coverage tools are available - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' run: | if ! command -v ccache >/dev/null 2>&1; then sudo apt-get install -y --no-install-recommends ccache @@ -368,7 +391,7 @@ jobs: fi - name: Verify coverage data files exist - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' working-directory: ${{ runner.temp }}/build run: | gcda_count=$(find . -name '*.gcda' | wc -l | tr -d '[:space:]') @@ -379,14 +402,14 @@ jobs: fi - name: Coverage Report - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/coverage with: build-dir: ${{ runner.temp }}/build mode: report-only - name: Verify Coverage Thresholds - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' working-directory: ${{ runner.temp }}/build run: | line_thresh=$(cmake -L -N . 2>/dev/null | sed -n 's/^QGC_COVERAGE_LINE_THRESHOLD:STRING=//p') From 06047802b707f9bd5933f987b077a3b630b56281 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 8 Mar 2026 16:18:44 -0700 Subject: [PATCH 055/119] Plan View: home position tracks map center, terrain-driven altitude, UI cleanup - Home position tracks map center when plan is empty; stops on user drag - Resume tracking when plan is cleared; set coordinate to map center on clear - Altitude field: read-only label when terrain data available, editable on failure - Add terrainQueryFailed property to VisualMissionItem - Remove _vehicleHasHomePosition visibility guard (always show in plan view) - Rename 'Launch Position' to 'Expected Home Position' - Simplify MissionSettingsItem: collapse setInitialHomePosition/setInitialHomePositionFromUser/setHomePositionFromVehicle into setCoordinate - Remove dead code: _centerHomePositionOnMissionItems, _plannedHomePositionMovedByUser, _plannedHomePositionFromVehicle - Rename _updateHomePosition to _updateFlyViewHomePosition - Hide GCS position marker in plan view - Extract PlanInfoEditor.qml from inline tree view delegate --- src/FlightMap/FlightMap.qml | 2 +- src/MissionManager/MissionController.cc | 47 +-- src/MissionManager/MissionController.h | 1 - src/MissionManager/MissionSettingsItem.cc | 51 +--- src/MissionManager/MissionSettingsItem.h | 14 +- src/MissionManager/VisualMissionItem.cc | 10 + src/MissionManager/VisualMissionItem.h | 4 + src/PlanView/CMakeLists.txt | 4 +- src/PlanView/MissionDefaultsEditor.qml | 153 ++++++++++ src/PlanView/MissionItemTreeView.qml | 279 ++--------------- src/PlanView/PlanInfoEditor.qml | 164 ++++++++++ src/PlanView/PlanTreeView.qml | 305 +++++++++++++++++++ src/PlanView/PlanView.qml | 30 ++ src/PlanView/PlanViewRightPanel.qml | 6 +- test/MissionManager/MissionControllerTest.cc | 8 +- 15 files changed, 714 insertions(+), 364 deletions(-) create mode 100644 src/PlanView/MissionDefaultsEditor.qml create mode 100644 src/PlanView/PlanInfoEditor.qml create mode 100644 src/PlanView/PlanTreeView.qml diff --git a/src/FlightMap/FlightMap.qml b/src/FlightMap/FlightMap.qml index 0786c2e174e0..58c9ed2a792b 100644 --- a/src/FlightMap/FlightMap.qml +++ b/src/FlightMap/FlightMap.qml @@ -229,7 +229,7 @@ Map { MapQuickItem { anchorPoint.x: sourceItem.width / 2 anchorPoint.y: sourceItem.height / 2 - visible: gcsPosition.isValid + visible: gcsPosition.isValid && !planView coordinate: gcsPosition sourceItem: Image { diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 8eac65e289a1..ea0bb2a467b6 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -171,7 +171,7 @@ void MissionController::_newMissionItemsAvailableFromVehicle(bool removeAllReque // First item is fake home position MissionItem* fakeHomeItem = newMissionItems[0]; if (fakeHomeItem->coordinate().latitude() != 0 || fakeHomeItem->coordinate().longitude() != 0) { - settingsItem->setInitialHomePosition(fakeHomeItem->coordinate()); + settingsItem->setCoordinate(fakeHomeItem->coordinate()); } i = 1; } @@ -670,7 +670,7 @@ bool MissionController::_loadJsonMissionFileV1(const QJsonObject& json, QmlObjec if (json.contains(_jsonPlannedHomePositionKey)) { SimpleMissionItem* item = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */); if (item->load(json[_jsonPlannedHomePositionKey].toObject(), 0, errorString)) { - settingsItem->setInitialHomePositionFromUser(item->coordinate()); + settingsItem->setCoordinate(item->coordinate()); item->deleteLater(); } else { return false; @@ -957,7 +957,7 @@ bool MissionController::_loadTextMissionFile(QTextStream& stream, QmlObjectListM SimpleMissionItem* item = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */); if (item->load(stream)) { if (firstItem && plannedHomePositionInFile) { - settingsItem->setInitialHomePositionFromUser(item->coordinate()); + settingsItem->setCoordinate(item->coordinate()); } else { if (TakeoffMissionItem::isTakeoffCommand(static_cast(item->command()))) { // This needs to be a TakeoffMissionItem @@ -1823,7 +1823,7 @@ void MissionController::_setupTreeModel(void) _visualItemsTree.clear(); // ── Plan File group ── - _planFileGroupNode.setObjectName(tr("Plan File")); + _planFileGroupNode.setObjectName(tr("Plan Info")); _planFileGroupIndex = QPersistentModelIndex( _visualItemsTree.appendItem(&_planFileGroupNode, QModelIndex(), QStringLiteral("planFileGroup"))); @@ -1995,7 +1995,7 @@ void MissionController::_setPlannedHomePositionFromFirstCoordinate(const QGeoCoo if (firstCoordinate.isValid()) { QGeoCoordinate plannedHomeCoord = firstCoordinate.atDistanceAndAzimuth(30, 0); plannedHomeCoord.setAltitude(0); - _settingsItem->setInitialHomePositionFromUser(plannedHomeCoord); + _settingsItem->setCoordinate(plannedHomeCoord); } } @@ -2255,43 +2255,6 @@ MissionSettingsItem* MissionController::_addMissionSettings(QmlObjectListModel* return settingsItem; } -void MissionController::_centerHomePositionOnMissionItems(QmlObjectListModel *visualItems) -{ - qCDebug(MissionControllerLog) << "_centerHomePositionOnMissionItems"; - - if (visualItems->count() > 1) { - double north = 0.0; - double south = 0.0; - double east = 0.0; - double west = 0.0; - bool firstCoordSet = false; - - for (int i=1; icount(); i++) { - VisualMissionItem* item = qobject_cast(visualItems->get(i)); - if (item->specifiesCoordinate()) { - if (firstCoordSet) { - double lat = _normalizeLat(item->entryCoordinate().latitude()); - double lon = _normalizeLon(item->entryCoordinate().longitude()); - north = fmax(north, lat); - south = fmin(south, lat); - east = fmax(east, lon); - west = fmin(west, lon); - } else { - firstCoordSet = true; - north = _normalizeLat(item->entryCoordinate().latitude()); - south = north; - east = _normalizeLon(item->entryCoordinate().longitude()); - west = east; - } - } - } - - if (firstCoordSet) { - _settingsItem->setInitialHomePositionFromUser(QGeoCoordinate((south + ((north - south) / 2)) - 90.0, (west + ((east - west) / 2)) - 180.0, 0.0)); - } - } -} - int MissionController::resumeMissionIndex(void) const { diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index bfabc7f55488..a4811e51fa3f 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -412,7 +412,6 @@ private slots: void _calcPrevWaypointValues (VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference); bool _findPreviousAltitude (int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltMode* prevAltMode); MissionSettingsItem* _addMissionSettings (QmlObjectListModel* visualItems); - void _centerHomePositionOnMissionItems (QmlObjectListModel* visualItems); bool _loadJsonMissionFile (const QByteArray& bytes, QmlObjectListModel* visualItems, QString& errorString); bool _loadJsonMissionFileV1 (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); bool _loadJsonMissionFileV2 (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); diff --git a/src/MissionManager/MissionSettingsItem.cc b/src/MissionManager/MissionSettingsItem.cc index fb97888e5f08..1586c6c5bfac 100644 --- a/src/MissionManager/MissionSettingsItem.cc +++ b/src/MissionManager/MissionSettingsItem.cc @@ -50,8 +50,8 @@ MissionSettingsItem::MissionSettingsItem(PlanMasterController* masterController, connect(&_plannedHomePositionAltitudeFact, &Fact::rawValueChanged, this, &MissionSettingsItem::_updateAltitudeInCoordinate); - connect(_managerVehicle, &Vehicle::homePositionChanged, this, &MissionSettingsItem::_updateHomePosition); - _updateHomePosition(_managerVehicle->homePosition()); + connect(_managerVehicle, &Vehicle::homePositionChanged, this, &MissionSettingsItem::_updateFlyViewHomePosition); + _updateFlyViewHomePosition(_managerVehicle->homePosition()); } int MissionSettingsItem::lastSequenceNumber(void) const @@ -173,52 +173,11 @@ void MissionSettingsItem::_setDirty(void) setDirty(true); } -void MissionSettingsItem::_setCoordinateWorker(const QGeoCoordinate& coordinate) +void MissionSettingsItem::setCoordinate(const QGeoCoordinate& coordinate) { if (_plannedHomePositionCoordinate != coordinate) { _plannedHomePositionCoordinate = coordinate; emit coordinateChanged(coordinate); - if (_plannedHomePositionFromVehicle) { - _plannedHomePositionAltitudeFact.setRawValue(coordinate.altitude()); - } - } -} - -void MissionSettingsItem::setHomePositionFromVehicle(Vehicle* vehicle) -{ - // If the user hasn't moved the planned home position manually we use the value from the vehicle - if (!_plannedHomePositionMovedByUser) { - QGeoCoordinate coordinate = vehicle->homePosition(); - // ArduPilot tends to send crap home positions at initial vehicle boot, discard them - if (coordinate.isValid() && (coordinate.latitude() != 0 || coordinate.longitude() != 0)) { - _plannedHomePositionFromVehicle = true; - _setCoordinateWorker(coordinate); - } - } -} - -void MissionSettingsItem::setInitialHomePosition(const QGeoCoordinate& coordinate) -{ - _plannedHomePositionMovedByUser = false; - _plannedHomePositionFromVehicle = false; - _setCoordinateWorker(coordinate); -} - -void MissionSettingsItem::setInitialHomePositionFromUser(const QGeoCoordinate& coordinate) -{ - _plannedHomePositionMovedByUser = true; - _plannedHomePositionFromVehicle = false; - _setCoordinateWorker(coordinate); -} - - -void MissionSettingsItem::setCoordinate(const QGeoCoordinate& coordinate) -{ - if (coordinate != this->coordinate()) { - // The user is moving the planned home position manually. Stop tracking vehicle home position. - _plannedHomePositionMovedByUser = true; - _plannedHomePositionFromVehicle = false; - _setCoordinateWorker(coordinate); } } @@ -267,7 +226,7 @@ double MissionSettingsItem::specifiedFlightSpeed(void) void MissionSettingsItem::_setHomeAltFromTerrain(double terrainAltitude) { - if (!_plannedHomePositionFromVehicle && !qIsNaN(terrainAltitude)) { + if (!qIsNaN(terrainAltitude)) { qCDebug(MissionSettingsItemLog) << "MissionSettingsItem::_setHomeAltFromTerrain" << terrainAltitude; _plannedHomePositionAltitudeFact.setRawValue(terrainAltitude); } @@ -278,7 +237,7 @@ QString MissionSettingsItem::abbreviation(void) const return _flyView ? tr("L") : tr("Launch"); } -void MissionSettingsItem::_updateHomePosition(const QGeoCoordinate& homePosition) +void MissionSettingsItem::_updateFlyViewHomePosition(const QGeoCoordinate& homePosition) { if (_flyView) { setCoordinate(homePosition); diff --git a/src/MissionManager/MissionSettingsItem.h b/src/MissionManager/MissionSettingsItem.h index d4fd19ba8da8..7b03566e6e23 100644 --- a/src/MissionManager/MissionSettingsItem.h +++ b/src/MissionManager/MissionSettingsItem.h @@ -38,15 +38,6 @@ class MissionSettingsItem : public ComplexMissionItem /// @return true: Mission end action was added bool addMissionEndAction(QList& items, int seqNum, QObject* missionItemParent); - /// Called to update home position coordinate when it comes from a connected vehicle - void setHomePositionFromVehicle(Vehicle* vehicle); - - // Called to set the initial home position. Vehicle can still update home position after this. - void setInitialHomePosition(const QGeoCoordinate& coordinate); - - // Called to set the initial home position specified by user. Vehicle will no longer affect home position. - void setInitialHomePositionFromUser(const QGeoCoordinate& coordinate); - // Overrides from ComplexMissionItem QString patternName (void) const final { return QString(); } double complexDistance (void) const final; @@ -96,16 +87,13 @@ private slots: void _sectionDirtyChanged (bool dirty); void _updateAltitudeInCoordinate (QVariant value); void _setHomeAltFromTerrain (double terrainAltitude); - void _setCoordinateWorker (const QGeoCoordinate& coordinate); - void _updateHomePosition (const QGeoCoordinate& homePosition); + void _updateFlyViewHomePosition (const QGeoCoordinate& homePosition); private: Vehicle* _managerVehicle = nullptr; QGeoCoordinate _plannedHomePositionCoordinate; // Does not include altitude Fact _plannedHomePositionAltitudeFact; int _sequenceNumber = 0; - bool _plannedHomePositionFromVehicle = false; - bool _plannedHomePositionMovedByUser = false; CameraSection _cameraSection; SpeedSection _speedSection; diff --git a/src/MissionManager/VisualMissionItem.cc b/src/MissionManager/VisualMissionItem.cc index 44ce7166e130..7037321356ea 100644 --- a/src/MissionManager/VisualMissionItem.cc +++ b/src/MissionManager/VisualMissionItem.cc @@ -165,6 +165,11 @@ void VisualMissionItem::_updateTerrainAltitude(void) _terrainAltitude = qQNaN(); emit terrainAltitudeChanged(qQNaN()); + if (_terrainQueryFailed) { + _terrainQueryFailed = false; + emit terrainQueryFailedChanged(_terrainQueryFailed); + } + if (!_flyView && specifiesCoordinate() && coordinate().isValid()) { // We use a timer so that any additional requests before the timer fires result in only a single request _updateTerrainTimer.start(); @@ -194,6 +199,11 @@ void VisualMissionItem::_terrainDataReceived(bool success, QList heights _terrainAltitude = success ? heights[0] : qQNaN(); emit terrainAltitudeChanged(_terrainAltitude); _currentTerrainAtCoordinateQuery = nullptr; + + if (_terrainQueryFailed != !success) { + _terrainQueryFailed = !success; + emit terrainQueryFailedChanged(_terrainQueryFailed); + } } void VisualMissionItem::_setBoundingCube(QGCGeoBoundingCube bc) diff --git a/src/MissionManager/VisualMissionItem.h b/src/MissionManager/VisualMissionItem.h index 51df5fd8a26e..b057e9f1530c 100644 --- a/src/MissionManager/VisualMissionItem.h +++ b/src/MissionManager/VisualMissionItem.h @@ -42,6 +42,7 @@ class VisualMissionItem : public QObject Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged) ///< Does not include altitude Q_PROPERTY(double amslEntryAlt READ amslEntryAlt NOTIFY amslEntryAltChanged) Q_PROPERTY(double terrainAltitude READ terrainAltitude NOTIFY terrainAltitudeChanged) ///< The altitude of terrain at the coordinate position, NaN if not known + Q_PROPERTY(bool terrainQueryFailed READ terrainQueryFailed NOTIFY terrainQueryFailedChanged) ///< true: Last terrain query failed Q_PROPERTY(QGeoCoordinate entryCoordinate READ entryCoordinate NOTIFY entryCoordinateChanged) ///< Does not include altitude Q_PROPERTY(QGeoCoordinate exitCoordinate READ exitCoordinate NOTIFY exitCoordinateChanged) ///< Does not include altitude Q_PROPERTY(double amslExitAlt READ amslExitAlt NOTIFY amslExitAltChanged) @@ -101,6 +102,7 @@ class VisualMissionItem : public QObject bool isCurrentItem (void) const { return _isCurrentItem; } bool hasCurrentChildItem (void) const { return _hasCurrentChildItem; } double terrainAltitude (void) const { return _terrainAltitude; } + bool terrainQueryFailed (void) const { return _terrainQueryFailed; } bool flyView (void) const { return _flyView; } bool wizardMode (void) const { return _wizardMode; } VisualMissionItem* parentItem(void) { return _parentItem; } @@ -227,6 +229,7 @@ class VisualMissionItem : public QObject void missionGimbalYawChanged (double missionGimbalYaw); void missionVehicleYawChanged (double missionVehicleYaw); void terrainAltitudeChanged (double terrainAltitude); + void terrainQueryFailedChanged (bool terrainQueryFailed); void additionalTimeDelayChanged (void); void boundingCubeChanged (void); void readyForSaveStateChanged (void); @@ -251,6 +254,7 @@ protected slots: bool _homePositionSpecialCase = false; ///< true: This item is being used as a ui home position indicator bool _wizardMode = false; ///< true: Item editor is showing wizard completion panel double _terrainAltitude = qQNaN(); ///< Altitude of terrain at coordinate position, NaN for not known + bool _terrainQueryFailed = false; ///< true: Last terrain query failed double _altDifference = 0; ///< Difference in altitude from previous waypoint double _altPercent = 0; ///< Percent of total altitude change in mission double _terrainPercent = qQNaN(); ///< Percent of terrain altitude for coordinate diff --git a/src/PlanView/CMakeLists.txt b/src/PlanView/CMakeLists.txt index 1ba911a8d1d0..ae9ecfa6fa17 100644 --- a/src/PlanView/CMakeLists.txt +++ b/src/PlanView/CMakeLists.txt @@ -19,15 +19,17 @@ qt_add_qml_module(PlanViewModule FWLandingPatternMapVisual.qml GeoFenceEditor.qml GeoFenceMapVisuals.qml + MissionDefaultsEditor.qml MissionItemEditor.qml MissionItemIndexLabel.qml MissionItemMapVisual.qml MissionItemStatus.qml - MissionItemTreeView.qml MissionSettingsEditor.qml MissionStats.qml PlanEditToolbar.qml + PlanInfoEditor.qml PlanToolBarIndicators.qml + PlanTreeView.qml PlanView.qml PlanViewRightPanel.qml RallyPointEditorHeader.qml diff --git a/src/PlanView/MissionDefaultsEditor.qml b/src/PlanView/MissionDefaultsEditor.qml new file mode 100644 index 000000000000..5166c6b2c50f --- /dev/null +++ b/src/PlanView/MissionDefaultsEditor.qml @@ -0,0 +1,153 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls +import QGroundControl.PlanView + +Rectangle { + id: _root + + required property var missionController + required property var planMasterController + + property var _controllerVehicle: planMasterController.controllerVehicle + property var _visualItems: missionController.visualItems + property bool _noMissionItemsAdded: _visualItems ? _visualItems.count <= 1 : true + property var _settingsItem: _visualItems && _visualItems.count > 0 ? _visualItems.get(0) : null + property bool _showCruiseSpeed: _controllerVehicle ? !_controllerVehicle.multiRotor : false + property bool _showHoverSpeed: _controllerVehicle ? (_controllerVehicle.multiRotor || _controllerVehicle.vtol) : false + property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 16 + + width: parent ? parent.width : 0 + height: mainColumn.height + ScreenTools.defaultFontPixelHeight + color: qgcPal.windowShadeDark + + QGCPalette { id: qgcPal; colorGroupEnabled: _root.enabled } + + Connections { + target: _root._controllerVehicle + function onFirmwareTypeChanged() { + if (!_root._controllerVehicle.supports.terrainFrame + && _root.missionController.globalAltitudeMode === QGroundControl.AltitudeModeTerrainFrame) { + _root.missionController.globalAltitudeMode = QGroundControl.AltitudeModeCalcAboveTerrain + } + } + } + + Component { id: altModeDialogComponent; AltModeDialog { } } + + QGCPopupDialogFactory { + id: altModeDialogFactory + dialogComponent: altModeDialogComponent + } + + ColumnLayout { + id: mainColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: ScreenTools.defaultFontPixelWidth + spacing: ScreenTools.defaultFontPixelHeight * 0.5 + + LabelledButton { + Layout.fillWidth: true + label: qsTr("Altitude Mode") + buttonText: QGroundControl.altitudeModeShortDescription(_root.missionController.globalAltitudeMode) + + onClicked: { + let removeModes = [] + let updateFunction = function(altMode) { _root.missionController.globalAltitudeMode = altMode } + if (!_root._controllerVehicle.supports.terrainFrame) { + removeModes.push(QGroundControl.AltitudeModeTerrainFrame) + } + if (!_root._noMissionItemsAdded) { + if (_root.missionController.globalAltitudeMode !== QGroundControl.AltitudeModeRelative) { + removeModes.push(QGroundControl.AltitudeModeRelative) + } + if (_root.missionController.globalAltitudeMode !== QGroundControl.AltitudeModeAbsolute) { + removeModes.push(QGroundControl.AltitudeModeAbsolute) + } + if (_root.missionController.globalAltitudeMode !== QGroundControl.AltitudeModeCalcAboveTerrain) { + removeModes.push(QGroundControl.AltitudeModeCalcAboveTerrain) + } + if (_root.missionController.globalAltitudeMode !== QGroundControl.AltitudeModeTerrainFrame) { + removeModes.push(QGroundControl.AltitudeModeTerrainFrame) + } + } + altModeDialogFactory.open({ rgRemoveModes: removeModes, updateAltModeFn: updateFunction }) + } + } + + FactTextFieldSlider { + Layout.fillWidth: true + label: qsTr("Waypoints Altitude") + fact: QGroundControl.settingsManager.appSettings.defaultMissionItemAltitude + } + + FactTextFieldSlider { + Layout.fillWidth: true + label: qsTr("Flight Speed") + fact: _root._settingsItem ? _root._settingsItem.speedSection.flightSpeed : null + showEnableCheckbox: true + enableCheckBoxChecked: _root._settingsItem ? _root._settingsItem.speedSection.specifyFlightSpeed : false + visible: _root._settingsItem ? _root._settingsItem.speedSection.available : false + + onEnableCheckboxClicked: { + if (_root._settingsItem) { + _root._settingsItem.speedSection.specifyFlightSpeed = enableCheckBoxChecked + } + } + } + + // ── Vehicle Speeds ── + SectionHeader { + id: vehicleSpeedsSectionHeader + Layout.fillWidth: true + text: qsTr("Vehicle Speeds") + visible: _root._showCruiseSpeed || _root._showHoverSpeed + checked: false + } + + GridLayout { + Layout.fillWidth: true + columnSpacing: ScreenTools.defaultFontPixelWidth + rowSpacing: columnSpacing + columns: 2 + visible: vehicleSpeedsSectionHeader.visible && vehicleSpeedsSectionHeader.checked + + QGCLabel { + Layout.columnSpan: 2 + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("The following speed values are used to calculate total mission time. They do not affect the flight speed for the mission.") + } + + QGCLabel { + text: qsTr("Cruise speed") + visible: _root._showCruiseSpeed + Layout.fillWidth: true + } + FactTextField { + fact: QGroundControl.settingsManager.appSettings.offlineEditingCruiseSpeed + visible: _root._showCruiseSpeed + Layout.preferredWidth: _root._fieldWidth + } + + QGCLabel { + text: qsTr("Hover speed") + visible: _root._showHoverSpeed + Layout.fillWidth: true + } + FactTextField { + fact: QGroundControl.settingsManager.appSettings.offlineEditingHoverSpeed + visible: _root._showHoverSpeed + Layout.preferredWidth: _root._fieldWidth + } + } + } +} diff --git a/src/PlanView/MissionItemTreeView.qml b/src/PlanView/MissionItemTreeView.qml index 66eea356566d..d9e01b529f98 100644 --- a/src/PlanView/MissionItemTreeView.qml +++ b/src/PlanView/MissionItemTreeView.qml @@ -89,6 +89,16 @@ TreeView { root.forceLayout() } + // Subtitle text shown on group headers, varies by node type + function _groupSubtitle(nodeType) { + switch (nodeType) { + case "planFileGroup": return planMasterController.currentPlanFileName + case "missionGroup": return _missionController.visualItems ? (_missionController.visualItems.count - 1) + qsTr(" items") : "" + case "rallyGroup": return _rallyPointController.points ? _rallyPointController.points.count + qsTr(" points") : "" + default: return "" + } + } + // Coalesces multiple delegate height changes into a single forceLayout() call Timer { id: layoutTimer @@ -176,6 +186,14 @@ TreeView { font.bold: true anchors.verticalCenter: parent.verticalCenter } + + QGCLabel { + text: root._groupSubtitle(delegateRoot.nodeType) + font.pointSize: ScreenTools.smallFontPointSize + color: qgcPal.colorGrey + anchors.verticalCenter: parent.verticalCenter + visible: text !== "" + } } MouseArea { @@ -185,42 +203,15 @@ TreeView { } } - // ── Plan file info delegate ── + // ── Plan info delegate ── Component { id: planFileInfoComponent - Rectangle { + PlanInfoEditor { width: delegateRoot.width - height: planFileColumn.height + ScreenTools.defaultFontPixelHeight - color: qgcPal.windowShadeDark - - Column { - id: planFileColumn - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: ScreenTools.defaultFontPixelWidth - spacing: ScreenTools.defaultFontPixelHeight * 0.25 - - QGCTextField { - id: planNameField - placeholderText: qsTr("Untitled") - width: parent.width - - Component.onCompleted: text = root.planMasterController.currentPlanFileName - - Connections { - target: root.planMasterController - function onCurrentPlanFileNameChanged() { - if (!planNameField.activeFocus) { - planNameField.text = root.planMasterController.currentPlanFileName - } - } - } - - onEditingFinished: root.planMasterController.currentPlanFileName = text - } - } + planMasterController: root.planMasterController + missionController: root._missionController + editorMap: root.editorMap } } @@ -228,228 +219,10 @@ TreeView { Component { id: defaultsEditorComponent - Rectangle { - id: defaultsRect + MissionDefaultsEditor { width: delegateRoot.width - height: defaultsColumn.height + ScreenTools.defaultFontPixelHeight - color: qgcPal.windowShadeDark - - property var _missionController: root._missionController - property var _controllerVehicle: root.planMasterController.controllerVehicle - property var _visualItems: root._missionController.visualItems - property bool _noMissionItemsAdded: _visualItems ? _visualItems.count <= 1 : true - property var _settingsItem: _visualItems && _visualItems.count > 0 ? _visualItems.get(0) : null - property bool _multipleFirmware: !QGroundControl.singleFirmwareSupport - property bool _multipleVehicleTypes: !QGroundControl.singleVehicleSupport - property bool _allowFWVehicleTypeSelection: _noMissionItemsAdded && !globals.activeVehicle - property bool _showCruiseSpeed: _controllerVehicle ? !_controllerVehicle.multiRotor : false - property bool _showHoverSpeed: _controllerVehicle ? (_controllerVehicle.multiRotor || _controllerVehicle.vtol) : false - property bool _vehicleHasHomePosition: _controllerVehicle ? _controllerVehicle.homePosition.isValid : false - property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly - property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 16 - readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2 - - Connections { - target: defaultsRect._controllerVehicle - function onFirmwareTypeChanged() { - if (!defaultsRect._controllerVehicle.supports.terrainFrame - && defaultsRect._missionController.globalAltitudeMode === QGroundControl.AltitudeModeTerrainFrame) { - defaultsRect._missionController.globalAltitudeMode = QGroundControl.AltitudeModeCalcAboveTerrain - } - } - } - - Component { id: altModeDialogComponent; AltModeDialog { } } - - QGCPopupDialogFactory { - id: defaultsAltModeDialogFactory - dialogComponent: altModeDialogComponent - } - - ColumnLayout { - id: defaultsColumn - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: ScreenTools.defaultFontPixelWidth - spacing: ScreenTools.defaultFontPixelHeight * 0.5 - - LabelledButton { - Layout.fillWidth: true - label: qsTr("Altitude Mode") - buttonText: QGroundControl.altitudeModeShortDescription(defaultsRect._missionController.globalAltitudeMode) - - onClicked: { - let removeModes = [] - let updateFunction = function(altMode) { defaultsRect._missionController.globalAltitudeMode = altMode } - if (!defaultsRect._controllerVehicle.supports.terrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) - } - if (!defaultsRect._noMissionItemsAdded) { - if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeRelative) { - removeModes.push(QGroundControl.AltitudeModeRelative) - } - if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeAbsolute) { - removeModes.push(QGroundControl.AltitudeModeAbsolute) - } - if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeCalcAboveTerrain) { - removeModes.push(QGroundControl.AltitudeModeCalcAboveTerrain) - } - if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeTerrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) - } - } - defaultsAltModeDialogFactory.open({ rgRemoveModes: removeModes, updateAltModeFn: updateFunction }) - } - } - - FactTextFieldSlider { - Layout.fillWidth: true - label: qsTr("Waypoints Altitude") - fact: QGroundControl.settingsManager.appSettings.defaultMissionItemAltitude - } - - FactTextFieldSlider { - Layout.fillWidth: true - label: qsTr("Flight Speed") - fact: defaultsRect._settingsItem ? defaultsRect._settingsItem.speedSection.flightSpeed : null - showEnableCheckbox: true - enableCheckBoxChecked: defaultsRect._settingsItem ? defaultsRect._settingsItem.speedSection.specifyFlightSpeed : false - visible: defaultsRect._settingsItem ? defaultsRect._settingsItem.speedSection.available : false - - onEnableCheckboxClicked: { - if (defaultsRect._settingsItem) { - defaultsRect._settingsItem.speedSection.specifyFlightSpeed = enableCheckBoxChecked - } - } - } - - // ── Vehicle Info ── - SectionHeader { - id: vehicleInfoSectionHeader - Layout.fillWidth: true - text: qsTr("Vehicle Info") - visible: !defaultsRect._waypointsOnlyMode - checked: false - } - - GridLayout { - Layout.fillWidth: true - columnSpacing: ScreenTools.defaultFontPixelWidth - rowSpacing: columnSpacing - columns: 2 - visible: vehicleInfoSectionHeader.visible && vehicleInfoSectionHeader.checked - - QGCLabel { - text: qsTr("Firmware") - Layout.fillWidth: true - visible: defaultsRect._multipleFirmware - } - FactComboBox { - fact: QGroundControl.settingsManager.appSettings.offlineEditingFirmwareClass - indexModel: false - Layout.preferredWidth: defaultsRect._fieldWidth - visible: defaultsRect._multipleFirmware && defaultsRect._allowFWVehicleTypeSelection - } - QGCLabel { - text: defaultsRect._controllerVehicle ? defaultsRect._controllerVehicle.firmwareTypeString : "" - visible: defaultsRect._multipleFirmware && !defaultsRect._allowFWVehicleTypeSelection - } - - QGCLabel { - text: qsTr("Vehicle") - Layout.fillWidth: true - visible: defaultsRect._multipleVehicleTypes - } - FactComboBox { - fact: QGroundControl.settingsManager.appSettings.offlineEditingVehicleClass - indexModel: false - Layout.preferredWidth: defaultsRect._fieldWidth - visible: defaultsRect._multipleVehicleTypes && defaultsRect._allowFWVehicleTypeSelection - } - QGCLabel { - text: defaultsRect._controllerVehicle ? defaultsRect._controllerVehicle.vehicleTypeString : "" - visible: defaultsRect._multipleVehicleTypes && !defaultsRect._allowFWVehicleTypeSelection - } - - QGCLabel { - Layout.columnSpan: 2 - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - wrapMode: Text.WordWrap - font.pointSize: ScreenTools.smallFontPointSize - text: qsTr("The following speed values are used to calculate total mission time. They do not affect the flight speed for the mission.") - visible: defaultsRect._showCruiseSpeed || defaultsRect._showHoverSpeed - } - - QGCLabel { - text: qsTr("Cruise speed") - visible: defaultsRect._showCruiseSpeed - Layout.fillWidth: true - } - FactTextField { - fact: QGroundControl.settingsManager.appSettings.offlineEditingCruiseSpeed - visible: defaultsRect._showCruiseSpeed - Layout.preferredWidth: defaultsRect._fieldWidth - } - - QGCLabel { - text: qsTr("Hover speed") - visible: defaultsRect._showHoverSpeed - Layout.fillWidth: true - } - FactTextField { - fact: QGroundControl.settingsManager.appSettings.offlineEditingHoverSpeed - visible: defaultsRect._showHoverSpeed - Layout.preferredWidth: defaultsRect._fieldWidth - } - } - - // ── Launch Position ── - SectionHeader { - id: plannedHomePositionSection - Layout.fillWidth: true - text: qsTr("Launch Position") - visible: !defaultsRect._vehicleHasHomePosition - checked: false - } - - GridLayout { - Layout.fillWidth: true - columnSpacing: ScreenTools.defaultFontPixelWidth - rowSpacing: columnSpacing - columns: 2 - visible: plannedHomePositionSection.checked && !defaultsRect._vehicleHasHomePosition - - QGCLabel { - text: qsTr("Altitude") - } - FactTextField { - fact: defaultsRect._settingsItem ? defaultsRect._settingsItem.plannedHomePositionAltitude : null - Layout.fillWidth: true - } - } - - QGCLabel { - Layout.fillWidth: true - wrapMode: Text.WordWrap - font.pointSize: ScreenTools.smallFontPointSize - text: qsTr("Actual position set by vehicle at flight time.") - horizontalAlignment: Text.AlignHCenter - visible: plannedHomePositionSection.checked && !defaultsRect._vehicleHasHomePosition - } - - QGCButton { - text: qsTr("Set To Map Center") - Layout.alignment: Qt.AlignHCenter - visible: plannedHomePositionSection.checked && !defaultsRect._vehicleHasHomePosition - onClicked: { - if (defaultsRect._settingsItem) { - defaultsRect._settingsItem.coordinate = root.editorMap.center - } - } - } - } + missionController: root._missionController + planMasterController: root.planMasterController } } diff --git a/src/PlanView/PlanInfoEditor.qml b/src/PlanView/PlanInfoEditor.qml new file mode 100644 index 000000000000..10eb1bde1666 --- /dev/null +++ b/src/PlanView/PlanInfoEditor.qml @@ -0,0 +1,164 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls + +Rectangle { + id: _root + + required property var planMasterController + required property var missionController + required property var editorMap + + property var _controllerVehicle: planMasterController.controllerVehicle + property var _visualItems: missionController.visualItems + property bool _noMissionItemsAdded: _visualItems ? _visualItems.count <= 1 : true + property var _settingsItem: _visualItems && _visualItems.count > 0 ? _visualItems.get(0) : null + property bool _multipleFirmware: !QGroundControl.singleFirmwareSupport + property bool _multipleVehicleTypes: !QGroundControl.singleVehicleSupport + property bool _allowFWVehicleTypeSelection: _noMissionItemsAdded && !globals.activeVehicle + property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly + property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 16 + + width: parent ? parent.width : 0 + height: mainColumn.height + ScreenTools.defaultFontPixelHeight + color: qgcPal.windowShadeDark + + QGCPalette { id: qgcPal; colorGroupEnabled: _root.enabled } + + ColumnLayout { + id: mainColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: ScreenTools.defaultFontPixelWidth + spacing: ScreenTools.defaultFontPixelHeight * 0.25 + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + QGCLabel { + text: qsTr("Plan File") + } + + QGCTextField { + id: planNameField + placeholderText: qsTr("Untitled") + Layout.fillWidth: true + + Component.onCompleted: text = _root.planMasterController.currentPlanFileName + + Connections { + target: _root.planMasterController + function onCurrentPlanFileNameChanged() { + if (!planNameField.activeFocus) { + planNameField.text = _root.planMasterController.currentPlanFileName + } + } + } + + onEditingFinished: _root.planMasterController.currentPlanFileName = text + } + } + + // ── Vehicle Info ── + SectionHeader { + id: vehicleInfoSectionHeader + Layout.fillWidth: true + text: qsTr("Vehicle Info") + visible: !_root._waypointsOnlyMode + } + + GridLayout { + Layout.fillWidth: true + columnSpacing: ScreenTools.defaultFontPixelWidth + rowSpacing: columnSpacing + columns: 2 + visible: vehicleInfoSectionHeader.visible && vehicleInfoSectionHeader.checked + + QGCLabel { + text: qsTr("Firmware") + Layout.fillWidth: true + visible: _root._multipleFirmware + } + FactComboBox { + fact: QGroundControl.settingsManager.appSettings.offlineEditingFirmwareClass + indexModel: false + Layout.preferredWidth: _root._fieldWidth + visible: _root._multipleFirmware && _root._allowFWVehicleTypeSelection + } + QGCLabel { + text: _root._controllerVehicle ? _root._controllerVehicle.firmwareTypeString : "" + visible: _root._multipleFirmware && !_root._allowFWVehicleTypeSelection + } + + QGCLabel { + text: qsTr("Vehicle") + Layout.fillWidth: true + visible: _root._multipleVehicleTypes + } + FactComboBox { + fact: QGroundControl.settingsManager.appSettings.offlineEditingVehicleClass + indexModel: false + Layout.preferredWidth: _root._fieldWidth + visible: _root._multipleVehicleTypes && _root._allowFWVehicleTypeSelection + } + QGCLabel { + text: _root._controllerVehicle ? _root._controllerVehicle.vehicleTypeString : "" + visible: _root._multipleVehicleTypes && !_root._allowFWVehicleTypeSelection + } + } + + // ── Expected Home Position ── + SectionHeader { + id: plannedHomePositionSection + Layout.fillWidth: true + text: qsTr("Expected Home Position") + } + + GridLayout { + Layout.fillWidth: true + columnSpacing: ScreenTools.defaultFontPixelWidth + columns: 2 + visible: plannedHomePositionSection.checked + + QGCLabel { + text: qsTr("Altitude (AMSL)") + } + FactTextField { + fact: _root._settingsItem ? _root._settingsItem.plannedHomePositionAltitude : null + Layout.fillWidth: true + visible: _root._settingsItem && _root._settingsItem.terrainQueryFailed + } + QGCLabel { + text: _root._settingsItem ? _root._settingsItem.plannedHomePositionAltitude.valueString + " " + _root._settingsItem.plannedHomePositionAltitude.units : "" + Layout.fillWidth: true + visible: !_root._settingsItem || !_root._settingsItem.terrainQueryFailed + } + } + + QGCLabel { + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Actual position/alt set by vehicle at flight time.") + horizontalAlignment: Text.AlignHCenter + visible: plannedHomePositionSection.checked + } + + QGCButton { + text: qsTr("Move To Map Center") + Layout.alignment: Qt.AlignHCenter + visible: plannedHomePositionSection.checked + onClicked: { + if (_root._settingsItem) { + _root._settingsItem.coordinate = _root.editorMap.center + } + } + } + } +} diff --git a/src/PlanView/PlanTreeView.qml b/src/PlanView/PlanTreeView.qml new file mode 100644 index 000000000000..406efea4a9e4 --- /dev/null +++ b/src/PlanView/PlanTreeView.qml @@ -0,0 +1,305 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls +import QGroundControl.PlanView + +/// Unified plan tree view showing Mission Items, GeoFence, and Rally Points +/// as collapsible sections using a real TreeView with type-discriminating delegates. +TreeView { + id: root + + required property var editorMap + required property var planMasterController + + signal editingLayerChangeRequested(int layer) + + readonly property int _layerMission: 1 + readonly property int _layerFence: 2 + readonly property int _layerRally: 3 + + property var _missionController: planMasterController.missionController + property var _geoFenceController: planMasterController.geoFenceController + property var _rallyPointController: planMasterController.rallyPointController + + model: _missionController.visualItemsTree + clip: true + boundsBehavior: Flickable.StopAtBounds + reuseItems: false + pointerNavigationEnabled: false + selectionBehavior: TableView.SelectionDisabled + rowSpacing: 2 + + // Helper: convert a persistent model index to the current visual row + function _rowFor(modelIndex) { return root.rowAtIndex(modelIndex) } + + // QGCFlickableScrollIndicator expects parent to have indicatorColor (provided by QGCFlickable/QGCListView) + property color indicatorColor: qgcPal.text + + QGCPalette { id: qgcPal; colorGroupEnabled: enabled } + + QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Horizontal } + QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Vertical } + + Connections { + target: root._missionController + function onVisualItemsChanged() { + // Mission group always expanded after rebuild (clear / load) + root.collapseRecursively() + root.expand(_rowFor(_missionController.missionGroupIndex)) + root.editingLayerChangeRequested(root._layerMission) + } + } + + // Public API: select a layer and expand its group. Called by the layer tool buttons. + function selectLayer(nodeType) { + let targetRow = -1 + switch (nodeType) { + case "missionGroup": + targetRow = _rowFor(_missionController.missionGroupIndex) + editingLayerChangeRequested(_layerMission) + break + case "fenceGroup": + targetRow = _rowFor(_missionController.fenceGroupIndex) + editingLayerChangeRequested(_layerFence) + break + case "rallyGroup": + targetRow = _rowFor(_missionController.rallyGroupIndex) + editingLayerChangeRequested(_layerRally) + break + } + + if (targetRow >= 0) { + if (!root.isExpanded(targetRow)) + root.expand(targetRow) + root.forceLayout() + root.positionViewAtRow(targetRow, TableView.AlignTop) + } + } + + // Toggle expand/collapse for a group header. Does not affect the editing layer. + function _toggleGroup(row) { + if (root.isExpanded(row)) + root.collapse(row) + else + root.expand(row) + root.forceLayout() + } + + // Subtitle text shown on group headers, varies by node type + function _groupSubtitle(nodeType) { + switch (nodeType) { + case "planFileGroup": return planMasterController.currentPlanFileName === "" ? qsTr("") : planMasterController.currentPlanFileName + case "missionGroup": return _missionController.visualItems ? (_missionController.visualItems.count - 1) + qsTr(" items") : "" + case "rallyGroup": return _rallyPointController.points ? _rallyPointController.points.count + qsTr(" points") : "" + default: return "" + } + } + + // Coalesces multiple delegate height changes into a single forceLayout() call + Timer { + id: layoutTimer + interval: 0 + running: false + repeat: false + onTriggered: root.forceLayout() + } + + delegate: Item { + id: delegateRoot + + required property TreeView treeView + required property bool isTreeNode + required property bool expanded + required property bool hasChildren + required property int depth + required property int row + required property var model + + readonly property var nodeObject: model.object + readonly property string nodeType: model.nodeType + + implicitWidth: root.width + implicitHeight: loader.item ? loader.item.height : 1 + width: root.width + height: implicitHeight + + onImplicitHeightChanged: layoutTimer.restart() + + Loader { + id: loader + width: parent.width + sourceComponent: { + // Guard: non-group delegates need a valid object. During model + // row removal the role data goes null before the delegate is + // destroyed, which would cause "Cannot read property of null" + // warnings in every downstream editor binding. + switch (delegateRoot.nodeType) { + case "planFileGroup": return groupHeaderComponent + case "defaultsGroup": return groupHeaderComponent + case "missionGroup": return groupHeaderComponent + case "fenceGroup": return groupHeaderComponent + case "rallyGroup": return groupHeaderComponent + case "transformGroup": return groupHeaderComponent + case "planFileInfo": return planFileInfoComponent + case "defaultsInfo": return defaultsEditorComponent + case "missionItem": return delegateRoot.nodeObject ? missionItemComponent : null + case "fenceEditor": return delegateRoot.nodeObject ? fenceEditorComponent : null + case "rallyHeader": return delegateRoot.nodeObject ? rallyHeaderComponent : null + case "rallyItem": return delegateRoot.nodeObject ? rallyItemComponent : null + case "transformEditor": return transformEditorComponent + default: return null + } + } + } + + // ── Group header (Mission Items / GeoFence / Rally Points) ── + Component { + id: groupHeaderComponent + + Rectangle { + width: delegateRoot.width + height: ScreenTools.implicitComboBoxHeight + ScreenTools.defaultFontPixelWidth + color: qgcPal.windowShade + + RowLayout { + id: groupHeaderRow + spacing: ScreenTools.defaultFontPixelWidth * 0.5 + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: ScreenTools.defaultFontPixelWidth * 0.5 + + QGCColoredImage { + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 0.75 + Layout.preferredHeight: Layout.preferredWidth + source: "/InstrumentValueIcons/cheveron-right.svg" + color: qgcPal.text + rotation: delegateRoot.expanded ? 90 : 0 + } + + QGCLabel { + Layout.alignment: Qt.AlignBaseline + text: delegateRoot.nodeObject ? delegateRoot.nodeObject.objectName : "" + font.bold: true + } + + QGCLabel { + Layout.alignment: Qt.AlignBaseline + Layout.fillWidth: true + text: root._groupSubtitle(delegateRoot.nodeType) + elide: Text.ElideRight + font.pointSize: ScreenTools.smallFontPointSize + color: qgcPal.colorGrey + } + } + + MouseArea { + anchors.fill: parent + onClicked: root._toggleGroup(delegateRoot.row) + } + } + } + + // ── Plan info delegate ── + Component { + id: planFileInfoComponent + + PlanInfoEditor { + width: delegateRoot.width + planMasterController: root.planMasterController + missionController: root._missionController + editorMap: root.editorMap + } + } + + // ── Defaults editor delegate ── + Component { + id: defaultsEditorComponent + + MissionDefaultsEditor { + width: delegateRoot.width + missionController: root._missionController + planMasterController: root.planMasterController + } + } + + // ── Mission item delegate ── + Component { + id: missionItemComponent + + MissionItemEditor { + width: delegateRoot.width + map: root.editorMap + masterController: root.planMasterController + missionItem: delegateRoot.nodeObject + readOnly: false + + onClicked: root._missionController.setCurrentPlanViewSeqNum(delegateRoot.nodeObject.sequenceNumber, false) + + onRemove: { + var viIndex = root._missionController.visualItemIndexForObject(delegateRoot.nodeObject) + if (viIndex > 0) { + root._missionController.removeVisualItem(viIndex) + } + } + + onSelectNextNotReadyItem: { + for (var i = 0; i < root._missionController.visualItems.count; i++) { + var vmi = root._missionController.visualItems.get(i) + if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) { + root._missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true) + break + } + } + } + } + } + + // ── GeoFence editor (single child of fence group) ── + Component { + id: fenceEditorComponent + + GeoFenceEditor { + width: delegateRoot.width + myGeoFenceController: root._geoFenceController + flightMap: root.editorMap + } + } + + // ── Rally header / instructions ── + Component { + id: rallyHeaderComponent + + RallyPointEditorHeader { + width: delegateRoot.width + controller: root._rallyPointController + } + } + + // ── Rally point item editor ── + Component { + id: rallyItemComponent + + RallyPointItemEditor { + width: delegateRoot.width + rallyPoint: delegateRoot.nodeObject + controller: root._rallyPointController + } + } + + // ── Transform editor (single child of transform group) ── + Component { + id: transformEditorComponent + + TransformEditor { + width: delegateRoot.width + missionController: root._missionController + } + } + } +} diff --git a/src/PlanView/PlanView.qml b/src/PlanView/PlanView.qml index 45e07d2e44e9..02c91f7bf53c 100644 --- a/src/PlanView/PlanView.qml +++ b/src/PlanView/PlanView.qml @@ -33,6 +33,8 @@ Item { property bool _promptForPlanUsageShowing: false property bool _addROIOnClick: false property bool _addWaypointOnClick: false + property bool _homeTrackingMapCenter: true + property bool _updatingHomeFromMapCenter: false readonly property int _layerMission: 1 readonly property int _layerFence: 2 @@ -160,6 +162,29 @@ Item { } } + // Stop tracking map center when the home position is changed externally (e.g. drag, file load) + Connections { + target: _visualItems.get(0) + function onCoordinateChanged() { + if (!_updatingHomeFromMapCenter && !_planMasterController.containsItems) { + _homeTrackingMapCenter = false + } + } + } + + // Resume tracking when the plan becomes empty again + Connections { + target: _planMasterController + function onContainsItemsChanged() { + if (!_planMasterController.containsItems) { + _homeTrackingMapCenter = true + _updatingHomeFromMapCenter = true + _visualItems.get(0).coordinate = editorMap.center + _updatingHomeFromMapCenter = false + } + } + } + function insertSimpleItemAfterCurrent(coordinate) { var nextIndex = _missionController.currentPlanViewVIIndex + 1 _missionController.insertSimpleMissionItem(coordinate, nextIndex, true /* makeCurrentItem */) @@ -254,6 +279,11 @@ Item { } onCenterChanged: { QGroundControl.flightMapPosition = editorMap.center + if (_homeTrackingMapCenter && !_planMasterController.containsItems) { + _updatingHomeFromMapCenter = true + _visualItems.get(0).coordinate = editorMap.center + _updatingHomeFromMapCenter = false + } } onMapClicked: (mouse) => { diff --git a/src/PlanView/PlanViewRightPanel.qml b/src/PlanView/PlanViewRightPanel.qml index 7531dfe2f9c2..cd4ce1affe70 100644 --- a/src/PlanView/PlanViewRightPanel.qml +++ b/src/PlanView/PlanViewRightPanel.qml @@ -90,8 +90,8 @@ Item { anchors.fill: parent } - MissionItemTreeView { - id: missionItemTreeView + PlanTreeView { + id: planTreeView anchors.fill: parent editorMap: root.editorMap planMasterController: root.planMasterController @@ -105,6 +105,6 @@ Item { root.anchors.left = undefined root.anchors.right = root.parent.right } - missionItemTreeView.selectLayer(nodeType) + planTreeView.selectLayer(nodeType) } } diff --git a/test/MissionManager/MissionControllerTest.cc b/test/MissionManager/MissionControllerTest.cc index 08a4f59a5694..bf5fc893eb17 100644 --- a/test/MissionManager/MissionControllerTest.cc +++ b/test/MissionManager/MissionControllerTest.cc @@ -202,7 +202,7 @@ void MissionControllerTest::_testMissionReposition() QVERIFY(settingsItem); const QGeoCoordinate home = Coord::zurich(); - settingsItem->setInitialHomePositionFromUser(home); + settingsItem->setCoordinate(home); const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(150.0, 15.0); const QGeoCoordinate wp2 = home.atDistanceAndAzimuth(320.0, 120.0); @@ -245,7 +245,7 @@ void MissionControllerTest::_testMissionOffset() QVERIFY(settingsItem); const QGeoCoordinate home = Coord::zurich(); - settingsItem->setInitialHomePositionFromUser(home); + settingsItem->setCoordinate(home); const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(150.0, 15.0); const QGeoCoordinate wp2 = home.atDistanceAndAzimuth(320.0, 120.0); @@ -280,7 +280,7 @@ void MissionControllerTest::_testMissionRotate() QVERIFY(settingsItem); const QGeoCoordinate home = Coord::zurich(); - settingsItem->setInitialHomePositionFromUser(home); + settingsItem->setCoordinate(home); const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(180.0, 20.0); const QGeoCoordinate wp2 = home.atDistanceAndAzimuth(260.0, 135.0); @@ -324,7 +324,7 @@ void MissionControllerTest::_testMissionTransformsInvalidHome() QVERIFY(settingsItem); const QGeoCoordinate home = Coord::zurich(); - settingsItem->setInitialHomePositionFromUser(home); + settingsItem->setCoordinate(home); const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(180.0, 45.0); _missionController->insertSimpleMissionItem(wp1, 1); From 28eee003ef8fd2a8c60400f3a8edf711be457b85 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 8 Mar 2026 19:56:12 -0700 Subject: [PATCH 056/119] Plan View: tree separator role, rally point editor rework Add SeparatorRole to QmlObjectTreeModel so tree delegates can draw visual dividers between sibling items. The role returns true for leaf nodes that are not the last child of their parent. dataChanged signals are emitted for affected siblings after insert/remove operations. Rework RallyPointItemEditor: - Fix collapsed height formula (was producing zero/negative height) - Replace hamburger menu with inline delete button - Add click-to-select on collapsed rally points - Switch to ColumnLayout + LabelledFactTextField for values - Shorten rally point fact metadata descriptions Delete the unused MissionItemTreeView.qml (superseded by PlanTreeView). Add separator visual line to PlanTreeView delegates. Reorder RallyPoint fields to lat/lon/alt convention. --- .../RallyPoint.FactMetaData.json | 12 +- src/MissionManager/RallyPoint.cc | 6 +- src/PlanView/MissionItemTreeView.qml | 303 ------------------ src/PlanView/PlanTreeView.qml | 36 ++- src/PlanView/RallyPointItemEditor.qml | 155 ++++----- src/QmlControls/QmlObjectTreeModel.cc | 35 +- src/QmlControls/QmlObjectTreeModel.h | 4 +- test/QmlControls/QmlObjectTreeModelTest.cc | 47 +++ test/QmlControls/QmlObjectTreeModelTest.h | 1 + 9 files changed, 186 insertions(+), 413 deletions(-) delete mode 100644 src/PlanView/MissionItemTreeView.qml diff --git a/src/MissionManager/RallyPoint.FactMetaData.json b/src/MissionManager/RallyPoint.FactMetaData.json index 789eaf061978..8bb1a7c81d2b 100644 --- a/src/MissionManager/RallyPoint.FactMetaData.json +++ b/src/MissionManager/RallyPoint.FactMetaData.json @@ -5,23 +5,23 @@ [ { "name": "Latitude", - "shortDesc": "Latitude of rally point position", + "shortDesc": "Latitude", "type": "double", - "decimalPlaces": 7 + "decimalPlaces": 6 }, { "name": "Longitude", - "shortDesc": "Longitude of rally point position", + "shortDesc": "Longitude", "type": "double", - "decimalPlaces": 7 + "decimalPlaces": 6 }, { "name": "RelativeAltitude", - "shortDesc": "Altitude of rally point position (home relative)", + "shortDesc": "Altitude (rel)", "type": "double", "decimalPlaces": 2, "units": "m", - "default": 0.0 + "default": 0.0 } ] } diff --git a/src/MissionManager/RallyPoint.cc b/src/MissionManager/RallyPoint.cc index 1205313244c2..4ba58b3635aa 100644 --- a/src/MissionManager/RallyPoint.cc +++ b/src/MissionManager/RallyPoint.cc @@ -48,16 +48,16 @@ void RallyPoint::_factSetup(void) { _cacheFactMetadata(); - _longitudeFact.setMetaData(_metaDataMap[_longitudeFactName]); _latitudeFact.setMetaData(_metaDataMap[_latitudeFactName]); + _longitudeFact.setMetaData(_metaDataMap[_longitudeFactName]); _altitudeFact.setMetaData(_metaDataMap[_altitudeFactName]); - _textFieldFacts.append(QVariant::fromValue(&_longitudeFact)); _textFieldFacts.append(QVariant::fromValue(&_latitudeFact)); + _textFieldFacts.append(QVariant::fromValue(&_longitudeFact)); _textFieldFacts.append(QVariant::fromValue(&_altitudeFact)); - connect(&_longitudeFact, &Fact::valueChanged, this, &RallyPoint::_sendCoordinateChanged); connect(&_latitudeFact, &Fact::valueChanged, this, &RallyPoint::_sendCoordinateChanged); + connect(&_longitudeFact, &Fact::valueChanged, this, &RallyPoint::_sendCoordinateChanged); connect(&_altitudeFact, &Fact::valueChanged, this, &RallyPoint::_sendCoordinateChanged); } diff --git a/src/PlanView/MissionItemTreeView.qml b/src/PlanView/MissionItemTreeView.qml deleted file mode 100644 index d9e01b529f98..000000000000 --- a/src/PlanView/MissionItemTreeView.qml +++ /dev/null @@ -1,303 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -import QGroundControl -import QGroundControl.Controls -import QGroundControl.FactControls -import QGroundControl.PlanView - -/// Unified plan tree view showing Mission Items, GeoFence, and Rally Points -/// as collapsible sections using a real TreeView with type-discriminating delegates. -TreeView { - id: root - - required property var editorMap - required property var planMasterController - - signal editingLayerChangeRequested(int layer) - - readonly property int _layerMission: 1 - readonly property int _layerFence: 2 - readonly property int _layerRally: 3 - - property var _missionController: planMasterController.missionController - property var _geoFenceController: planMasterController.geoFenceController - property var _rallyPointController: planMasterController.rallyPointController - - model: _missionController.visualItemsTree - clip: true - boundsBehavior: Flickable.StopAtBounds - reuseItems: false - pointerNavigationEnabled: false - selectionBehavior: TableView.SelectionDisabled - rowSpacing: 2 - - // Helper: convert a persistent model index to the current visual row - function _rowFor(modelIndex) { return root.rowAtIndex(modelIndex) } - - // QGCFlickableScrollIndicator expects parent to have indicatorColor (provided by QGCFlickable/QGCListView) - property color indicatorColor: qgcPal.text - - QGCPalette { id: qgcPal; colorGroupEnabled: enabled } - - QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Horizontal } - QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Vertical } - - Connections { - target: root._missionController - function onVisualItemsChanged() { - // Mission group always expanded after rebuild (clear / load) - root.collapseRecursively() - root.expand(_rowFor(_missionController.missionGroupIndex)) - root.editingLayerChangeRequested(root._layerMission) - } - } - - // Public API: select a layer and expand its group. Called by the layer tool buttons. - function selectLayer(nodeType) { - let targetRow = -1 - switch (nodeType) { - case "missionGroup": - targetRow = _rowFor(_missionController.missionGroupIndex) - editingLayerChangeRequested(_layerMission) - break - case "fenceGroup": - targetRow = _rowFor(_missionController.fenceGroupIndex) - editingLayerChangeRequested(_layerFence) - break - case "rallyGroup": - targetRow = _rowFor(_missionController.rallyGroupIndex) - editingLayerChangeRequested(_layerRally) - break - } - - if (targetRow >= 0) { - if (!root.isExpanded(targetRow)) - root.expand(targetRow) - root.forceLayout() - root.positionViewAtRow(targetRow, TableView.AlignTop) - } - } - - // Toggle expand/collapse for a group header. Does not affect the editing layer. - function _toggleGroup(row) { - if (root.isExpanded(row)) - root.collapse(row) - else - root.expand(row) - root.forceLayout() - } - - // Subtitle text shown on group headers, varies by node type - function _groupSubtitle(nodeType) { - switch (nodeType) { - case "planFileGroup": return planMasterController.currentPlanFileName - case "missionGroup": return _missionController.visualItems ? (_missionController.visualItems.count - 1) + qsTr(" items") : "" - case "rallyGroup": return _rallyPointController.points ? _rallyPointController.points.count + qsTr(" points") : "" - default: return "" - } - } - - // Coalesces multiple delegate height changes into a single forceLayout() call - Timer { - id: layoutTimer - interval: 0 - running: false - repeat: false - onTriggered: root.forceLayout() - } - - delegate: Item { - id: delegateRoot - - required property TreeView treeView - required property bool isTreeNode - required property bool expanded - required property bool hasChildren - required property int depth - required property int row - required property var model - - readonly property var nodeObject: model.object - readonly property string nodeType: model.nodeType - - implicitWidth: root.width - implicitHeight: loader.item ? loader.item.height : 1 - width: root.width - height: implicitHeight - - onImplicitHeightChanged: layoutTimer.restart() - - Loader { - id: loader - width: parent.width - sourceComponent: { - // Guard: non-group delegates need a valid object. During model - // row removal the role data goes null before the delegate is - // destroyed, which would cause "Cannot read property of null" - // warnings in every downstream editor binding. - switch (delegateRoot.nodeType) { - case "planFileGroup": return groupHeaderComponent - case "defaultsGroup": return groupHeaderComponent - case "missionGroup": return groupHeaderComponent - case "fenceGroup": return groupHeaderComponent - case "rallyGroup": return groupHeaderComponent - case "transformGroup": return groupHeaderComponent - case "planFileInfo": return planFileInfoComponent - case "defaultsInfo": return defaultsEditorComponent - case "missionItem": return delegateRoot.nodeObject ? missionItemComponent : null - case "fenceEditor": return delegateRoot.nodeObject ? fenceEditorComponent : null - case "rallyHeader": return delegateRoot.nodeObject ? rallyHeaderComponent : null - case "rallyItem": return delegateRoot.nodeObject ? rallyItemComponent : null - case "transformEditor": return transformEditorComponent - default: return null - } - } - } - - // ── Group header (Mission Items / GeoFence / Rally Points) ── - Component { - id: groupHeaderComponent - - Rectangle { - width: delegateRoot.width - height: ScreenTools.implicitComboBoxHeight + ScreenTools.defaultFontPixelWidth - color: qgcPal.windowShade - - Row { - id: groupHeaderRow - spacing: ScreenTools.defaultFontPixelWidth * 0.5 - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: ScreenTools.defaultFontPixelWidth * 0.5 - - QGCColoredImage { - width: ScreenTools.defaultFontPixelHeight * 0.75 - height: width - source: "/InstrumentValueIcons/cheveron-right.svg" - color: qgcPal.text - anchors.verticalCenter: parent.verticalCenter - rotation: delegateRoot.expanded ? 90 : 0 - } - - QGCLabel { - text: delegateRoot.nodeObject ? delegateRoot.nodeObject.objectName : "" - font.bold: true - anchors.verticalCenter: parent.verticalCenter - } - - QGCLabel { - text: root._groupSubtitle(delegateRoot.nodeType) - font.pointSize: ScreenTools.smallFontPointSize - color: qgcPal.colorGrey - anchors.verticalCenter: parent.verticalCenter - visible: text !== "" - } - } - - MouseArea { - anchors.fill: parent - onClicked: root._toggleGroup(delegateRoot.row) - } - } - } - - // ── Plan info delegate ── - Component { - id: planFileInfoComponent - - PlanInfoEditor { - width: delegateRoot.width - planMasterController: root.planMasterController - missionController: root._missionController - editorMap: root.editorMap - } - } - - // ── Defaults editor delegate ── - Component { - id: defaultsEditorComponent - - MissionDefaultsEditor { - width: delegateRoot.width - missionController: root._missionController - planMasterController: root.planMasterController - } - } - - // ── Mission item delegate ── - Component { - id: missionItemComponent - - MissionItemEditor { - width: delegateRoot.width - map: root.editorMap - masterController: root.planMasterController - missionItem: delegateRoot.nodeObject - readOnly: false - - onClicked: root._missionController.setCurrentPlanViewSeqNum(delegateRoot.nodeObject.sequenceNumber, false) - - onRemove: { - var viIndex = root._missionController.visualItemIndexForObject(delegateRoot.nodeObject) - if (viIndex > 0) { - root._missionController.removeVisualItem(viIndex) - } - } - - onSelectNextNotReadyItem: { - for (var i = 0; i < root._missionController.visualItems.count; i++) { - var vmi = root._missionController.visualItems.get(i) - if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) { - root._missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true) - break - } - } - } - } - } - - // ── GeoFence editor (single child of fence group) ── - Component { - id: fenceEditorComponent - - GeoFenceEditor { - width: delegateRoot.width - myGeoFenceController: root._geoFenceController - flightMap: root.editorMap - } - } - - // ── Rally header / instructions ── - Component { - id: rallyHeaderComponent - - RallyPointEditorHeader { - width: delegateRoot.width - controller: root._rallyPointController - } - } - - // ── Rally point item editor ── - Component { - id: rallyItemComponent - - RallyPointItemEditor { - width: delegateRoot.width - rallyPoint: delegateRoot.nodeObject - controller: root._rallyPointController - } - } - - // ── Transform editor (single child of transform group) ── - Component { - id: transformEditorComponent - - TransformEditor { - width: delegateRoot.width - missionController: root._missionController - } - } - } -} diff --git a/src/PlanView/PlanTreeView.qml b/src/PlanView/PlanTreeView.qml index 406efea4a9e4..5438b936200e 100644 --- a/src/PlanView/PlanTreeView.qml +++ b/src/PlanView/PlanTreeView.qml @@ -11,6 +11,13 @@ import QGroundControl.PlanView /// as collapsible sections using a real TreeView with type-discriminating delegates. TreeView { id: root + model: _missionController.visualItemsTree + clip: true + boundsBehavior: Flickable.StopAtBounds + reuseItems: false + pointerNavigationEnabled: false + selectionBehavior: TableView.SelectionDisabled + rowSpacing: 2 required property var editorMap required property var planMasterController @@ -25,14 +32,6 @@ TreeView { property var _geoFenceController: planMasterController.geoFenceController property var _rallyPointController: planMasterController.rallyPointController - model: _missionController.visualItemsTree - clip: true - boundsBehavior: Flickable.StopAtBounds - reuseItems: false - pointerNavigationEnabled: false - selectionBehavior: TableView.SelectionDisabled - rowSpacing: 2 - // Helper: convert a persistent model index to the current visual row function _rowFor(modelIndex) { return root.rowAtIndex(modelIndex) } @@ -110,6 +109,10 @@ TreeView { delegate: Item { id: delegateRoot + implicitWidth: root.width + implicitHeight: (loader.item ? loader.item.height : 1) + (separatorLine.visible ? separatorLine.height + root.rowSpacing : 0) + width: root.width + height: implicitHeight required property TreeView treeView required property bool isTreeNode @@ -121,11 +124,8 @@ TreeView { readonly property var nodeObject: model.object readonly property string nodeType: model.nodeType + readonly property bool separator: model.separator ?? false - implicitWidth: root.width - implicitHeight: loader.item ? loader.item.height : 1 - width: root.width - height: implicitHeight onImplicitHeightChanged: layoutTimer.restart() @@ -156,6 +156,18 @@ TreeView { } } + Rectangle { + id: separatorLine + anchors.margins: ScreenTools.defaultFontPixelWidth * 0.5 + anchors.topMargin: root.rowSpacing + anchors.top: loader.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: qgcPal.groupBorder + visible: delegateRoot.separator + } + // ── Group header (Mission Items / GeoFence / Rally Points) ── Component { id: groupHeaderComponent diff --git a/src/PlanView/RallyPointItemEditor.qml b/src/PlanView/RallyPointItemEditor.qml index 3486128565b4..89c2886f0dda 100644 --- a/src/PlanView/RallyPointItemEditor.qml +++ b/src/PlanView/RallyPointItemEditor.qml @@ -7,113 +7,94 @@ import QGroundControl.Controls import QGroundControl.FactControls Rectangle { - id: root - height: _currentItem ? valuesRect.y + valuesRect.height + (_margin * 2) : titleBar.y - titleBar.height + _margin - color: _currentItem ? qgcPal.buttonHighlight : qgcPal.windowShade + id: root + height: _currentItem ? valuesRect.y + valuesRect.height + _innerMargin : titleLayout.y + titleLayout.height + _margin + color: _currentItem ? qgcPal.buttonHighlight : qgcPal.windowShade radius: _radius - signal clicked() - property var rallyPoint ///< RallyPoint object associated with editor property var controller ///< RallyPointController - property bool _currentItem: rallyPoint ? rallyPoint === controller.currentRallyPoint : false - property color _outerTextColor: qgcPal.text // _currentItem ? "black" : qgcPal.text + property bool _currentItem: rallyPoint ? rallyPoint === controller.currentRallyPoint : false + property color _outerTextColor: qgcPal.text - readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2 - readonly property real _radius: ScreenTools.defaultFontPixelWidth / 2 - readonly property real _titleHeight: ScreenTools.defaultFontPixelHeight * 2 + readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2 + readonly property real _innerMargin: 2 + readonly property real _radius: ScreenTools.defaultFontPixelWidth / 2 + readonly property real _titleHeight: ScreenTools.implicitComboBoxHeight + ScreenTools.defaultFontPixelWidth QGCPalette { id: qgcPal; colorGroupEnabled: true } - Item { - id: titleBar - anchors.margins: _margin - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: _titleHeight - - MissionItemIndexLabel { - id: indicator - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - label: "R" - checked: true - } + RowLayout { + id: titleLayout + anchors.margins: _margin + anchors.left: parent.left + anchors.rightMargin: _margin * 2 + anchors.right: parent.right + height: _titleHeight + spacing: ScreenTools.defaultFontPixelWidth QGCLabel { - anchors.leftMargin: _margin - anchors.left: indicator.right - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Rally Point") - color: _outerTextColor + text: qsTr("Rally Point") + color: _outerTextColor } QGCColoredImage { - id: hamburger - anchors.rightMargin: _margin - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - width: ScreenTools.defaultFontPixelWidth * 2 - height: width - sourceSize.height: height - source: "qrc:/qmlimages/Hamburger.svg" - color: qgcPal.text - - MouseArea { - anchors.fill: parent - onClicked: hamburgerMenu.popup() - - QGCMenu { - id: hamburgerMenu - - QGCMenuItem { - text: qsTr("Delete") - onTriggered: controller.removePoint(rallyPoint) - } - } - } + id: deleteButton + Layout.alignment: Qt.AlignRight + Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 0.5 + Layout.preferredHeight: Layout.preferredWidth + source: "/res/XDelete.svg" + color: _outerTextColor } - } // Item - titleBar + } + + QGCMouseArea { + id: selectMouseArea + anchors.top: titleLayout.top + anchors.bottomMargin: -_margin + anchors.bottom: titleLayout.bottom + anchors.left: titleLayout.left + anchors.right: titleLayout.right + onClicked: controller.currentRallyPoint = rallyPoint + } + + QGCMouseArea { + anchors.top: selectMouseArea.top + anchors.bottom: selectMouseArea.bottom + anchors.right: selectMouseArea.right + width: height + onClicked: controller.removePoint(rallyPoint) + } Rectangle { - id: valuesRect - anchors.margins: _margin - anchors.left: parent.left - anchors.right: parent.right - anchors.top: titleBar.bottom - height: valuesGrid.height + (_margin * 2) - color: qgcPal.windowShadeDark - visible: _currentItem - radius: _radius - - GridLayout { - id: valuesGrid - anchors.margins: _margin - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - rowSpacing: _margin - columnSpacing: _margin - rows: rallyPoint ? rallyPoint.textFieldFacts.length : 0 - flow: GridLayout.TopToBottom + id: valuesRect + anchors.margins: _innerMargin + anchors.left: parent.left + anchors.right: parent.right + anchors.top: titleLayout.bottom + height: valuesLayout.height + (_margin * 2) + color: qgcPal.windowShadeDark + visible: _currentItem + radius: _radius + + ColumnLayout { + id: valuesLayout + anchors.margins: _margin + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: _margin Repeater { model: rallyPoint ? rallyPoint.textFieldFacts : 0 - QGCLabel { - text: modelData.name + ":" - } - } - Repeater { - model: rallyPoint ? rallyPoint.textFieldFacts : 0 - FactTextField { - Layout.fillWidth: true - showUnits: true - fact: modelData + LabelledFactTextField { + Layout.fillWidth: true + label: modelData.shortDescription + fact: modelData } } - } // GridLayout - } // Rectangle -} // Rectangle + } + } +} diff --git a/src/QmlControls/QmlObjectTreeModel.cc b/src/QmlControls/QmlObjectTreeModel.cc index 56fecb1ba10e..c56a328986ce 100644 --- a/src/QmlControls/QmlObjectTreeModel.cc +++ b/src/QmlControls/QmlObjectTreeModel.cc @@ -137,6 +137,16 @@ QVariant QmlObjectTreeModel::data(const QModelIndex& index, int role) const return node->object ? QVariant::fromValue(node->object->objectName()) : QVariant{}; case NodeTypeRole: return QVariant::fromValue(node->nodeType); + case SeparatorRole: { + if (!node->children.isEmpty()) { + return false; + } + const TreeNode* parentNode = node->parentNode; + if (!parentNode) { + return false; + } + return node->row() < parentNode->children.size() - 1; + } default: return {}; } @@ -183,7 +193,8 @@ bool QmlObjectTreeModel::removeRows(int /*row*/, int /*count*/, const QModelInde QHash QmlObjectTreeModel::roleNames() const { auto roles = ObjectItemModelBase::roleNames(); - roles[NodeTypeRole] = "nodeType"; + roles[NodeTypeRole] = "nodeType"; + roles[SeparatorRole] = "separator"; return roles; } @@ -264,6 +275,7 @@ QModelIndex QmlObjectTreeModel::insertItem(int row, QObject* object, const QMode beginInsertRows(parentIndex, row, row); parentNode->children.insert(row, node); endInsertRows(); + _emitSeparatorChanged(parentIndex, row > 0 ? row - 1 : 0); _signalCountChangedIfNotNested(); } @@ -315,6 +327,9 @@ QObject* QmlObjectTreeModel::removeItem(const QModelIndex& index) delete node; _totalCount -= removedCount; + if (!parentNode->children.isEmpty()) { + _emitSeparatorChanged(parentIdx, parentNode->children.count() - 1); + } _signalCountChangedIfNotNested(); setDirty(true); @@ -512,3 +527,21 @@ void QmlObjectTreeModel::_disconnectDirtyChanged(QObject* object) disconnect(object, signal, this, slot); } } + +void QmlObjectTreeModel::_emitSeparatorChanged(const QModelIndex& parentIdx, int fromRow) +{ + const TreeNode* parentNode = parentIdx.isValid() ? _nodeFromIndex(parentIdx) : &_rootNode; + if (!parentNode) { + return; + } + const int last = parentNode->children.count() - 1; + if (fromRow > last) { + fromRow = last; + } + if (fromRow < 0) { + return; + } + const QModelIndex first = index(fromRow, 0, parentIdx); + const QModelIndex end = index(last, 0, parentIdx); + emit dataChanged(first, end, {SeparatorRole}); +} diff --git a/src/QmlControls/QmlObjectTreeModel.h b/src/QmlControls/QmlObjectTreeModel.h index 48e6bd7ca9f7..ed71422bd38e 100644 --- a/src/QmlControls/QmlObjectTreeModel.h +++ b/src/QmlControls/QmlObjectTreeModel.h @@ -93,7 +93,8 @@ class QmlObjectTreeModel : public ObjectItemModelBase void clearAndDeleteContents(); ///< Clears the tree and calls deleteLater on every QObject bool contains(QObject* object) const; - static constexpr int NodeTypeRole = Qt::UserRole + 2; + static constexpr int NodeTypeRole = Qt::UserRole + 2; + static constexpr int SeparatorRole = Qt::UserRole + 3; private: struct TreeNode { @@ -114,6 +115,7 @@ class QmlObjectTreeModel : public ObjectItemModelBase void _deleteSubtree(TreeNode* node, bool deleteObjects); void _connectDirtyChanged(QObject* object); void _disconnectDirtyChanged(QObject* object); + void _emitSeparatorChanged(const QModelIndex& parentIdx, int fromRow); TreeNode _rootNode; ///< Invisible root; top-level items are its children int _totalCount = 0; ///< Cached total node count (all nodes in tree) diff --git a/test/QmlControls/QmlObjectTreeModelTest.cc b/test/QmlControls/QmlObjectTreeModelTest.cc index 645d23ce8c04..b31d5c29fb1d 100644 --- a/test/QmlControls/QmlObjectTreeModelTest.cc +++ b/test/QmlControls/QmlObjectTreeModelTest.cc @@ -54,6 +54,7 @@ void QmlObjectTreeModelTest::_roleNamesIncludeNodeType() QVERIFY(roles.values().contains("object")); QVERIFY(roles.values().contains("text")); QVERIFY(roles.values().contains("nodeType")); + QVERIFY(roles.values().contains("separator")); } // =========================================================================== @@ -435,6 +436,52 @@ void QmlObjectTreeModelTest::_dataNodeTypeRole() QCOMPARE(model.data(idx, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("missionItem")); } +void QmlObjectTreeModelTest::_dataSeparatorRole() +{ + QmlObjectTreeModel model; + + // Build a parent with three leaf children + QObject parent; + QObject child1, child2, child3; + const QModelIndex parentIdx = model.appendItem(&parent, QModelIndex(), QStringLiteral("group")); + const QModelIndex c1 = model.appendItem(&child1, parentIdx, QStringLiteral("item")); + const QModelIndex c2 = model.appendItem(&child2, parentIdx, QStringLiteral("item")); + const QModelIndex c3 = model.appendItem(&child3, parentIdx, QStringLiteral("item")); + + // First two children are not last → separator = true + QCOMPARE(model.data(c1, QmlObjectTreeModel::SeparatorRole).toBool(), true); + QCOMPARE(model.data(c2, QmlObjectTreeModel::SeparatorRole).toBool(), true); + // Last child → separator = false + QCOMPARE(model.data(c3, QmlObjectTreeModel::SeparatorRole).toBool(), false); + + // Parent node has children → separator = false regardless of position + QCOMPARE(model.data(parentIdx, QmlObjectTreeModel::SeparatorRole).toBool(), false); + + // Remove last child — c2 is now last and should emit dataChanged with SeparatorRole + QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged); + model.removeItem(c3); + + // c2 should now be the last child → separator = false + const QModelIndex c2New = model.index(1, 0, parentIdx); + QCOMPARE(model.data(c2New, QmlObjectTreeModel::SeparatorRole).toBool(), false); + + // Verify dataChanged was emitted with SeparatorRole + bool foundSeparatorChange = false; + for (const auto& call : dataChangedSpy) { + const QList roles = call.at(2).value>(); + if (roles.contains(QmlObjectTreeModel::SeparatorRole)) { + foundSeparatorChange = true; + break; + } + } + QVERIFY(foundSeparatorChange); + + // Insert a new child at the end — c2 should flip back to separator = true + QObject child4; + model.appendItem(&child4, parentIdx, QStringLiteral("item")); + QCOMPARE(model.data(c2New, QmlObjectTreeModel::SeparatorRole).toBool(), true); +} + void QmlObjectTreeModelTest::_dataInvalidIndexReturnsEmpty() { QmlObjectTreeModel model; diff --git a/test/QmlControls/QmlObjectTreeModelTest.h b/test/QmlControls/QmlObjectTreeModelTest.h index f397292355de..e7203771a544 100644 --- a/test/QmlControls/QmlObjectTreeModelTest.h +++ b/test/QmlControls/QmlObjectTreeModelTest.h @@ -52,6 +52,7 @@ private slots: void _dataObjectRole(); void _dataTextRole(); void _dataNodeTypeRole(); + void _dataSeparatorRole(); void _dataInvalidIndexReturnsEmpty(); void _setDataReplacesObject(); void _setDataNonObjectRoleReturnsFalse(); From 15238a05145a03003f429cec4d804abb6b37376f Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Tue, 10 Mar 2026 13:47:15 -0700 Subject: [PATCH 057/119] docs: Update Plan View documentation to match current UI - Replace plan_view_overview.jpg with updated PlanView.png screenshot - Rewrite UI Overview section to reflect current toolbar, tool strip, and panel layout - Rename 'Mission Command List' to 'Plan Editor Panel' throughout - Add new sections: Plan Info, Defaults, Mission Items, GeoFence, Rally Points, Transform - Add File Operations, Layer Switcher, and Mission Stats/Terrain Panel sections - Remove outdated screenshots pending new ones - Fix typos throughout the document --- docs/assets/plan/plan_view_overview.jpg | Bin 104915 -> 0 bytes docs/assets/plan/plan_view_overview.png | Bin 0 -> 2055185 bytes docs/en/qgc-user-guide/plan_view/plan_view.md | 195 +++++++++--------- 3 files changed, 95 insertions(+), 100 deletions(-) delete mode 100644 docs/assets/plan/plan_view_overview.jpg create mode 100644 docs/assets/plan/plan_view_overview.png diff --git a/docs/assets/plan/plan_view_overview.jpg b/docs/assets/plan/plan_view_overview.jpg deleted file mode 100644 index c7de9d5f7d5bc36dd285caeb406c37dc0fbfe040..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104915 zcmeFXWmH_vvp%|U2ojv&65N6f4hilM+}+(F3=$xL;I4zqV8h^pLkxEr+!EXgon%3ljqy3kwSe2m28oDLx)9E*>Q@$zxJlDtbCvDjFIF zW?nW1Chn&+H0&ZA+78qF`bFSq8BG6;zHuvGn`G|D*AL zub>Ezko`7!%oZH<11UJaxAsaLQ)3Q4G6e^}Mh(?(_OAT0wBP%NQ&!f~wj>mV!{h^6 zG0j1rFl-H%AV5>IK^zOMI86=jN{O79yca?C*vI}4K&a-=&F9B`8j>{__;`S8yUNs& z#NGALMPa8t)Q2Nqp-jX3@Q1G}tlq&uCa^nO8rLl^_`82d$@-q%{P5LdsdUu1f+Gph zYYO%G#X2tj&uM!G(uOytV;@?<*$D-C%(1l9?2C7=MI|#tGpY&fg@dq|BzDee4+~sl z72jFs%5_q1R3yx?~&LU4POoMPzn052Nu0ObkMoy5bs5D|3 zSmdel%^8gt_`Wi2mN(7QO3d^1#a2k@xK<^$NhdxxS?2oJH;CE-w0CTiO*SIFBIBuy zKFi>E?l-fStEQ?>K{gpFvK{^5fNxR*MeW37)cvvd%OikrX?pIR3WwgdqSJnZ@kD1i zHo-HstSb6Fy?15_@2GT&+XWv<2JT8;iSJ9!l%iv{WVU6DAxn4XuEh(nU7G4f_IeJJ z&ZSyLQ%g+D*z!#yBUBHFn~F5lDzbRg^GulZou>)7VlWGROH;DeEV4PmH7hD`DfoFb zHI5#)h$BPNYf`1YlWC4gSeo?6iTG((?{rV(E{OWp%2%Hz$o(RqUH{Q-=R}rD+X(!tY2SuC~c?q&6{fa{gq6TyiKBqvJ%qI;*zWehw)w&cDG@Z7(3!*px94m3a@vIFZ3zl}E!jxEQ zmiNg)U64xm^Kg&4ET(bj@;SGSuzc72!{@`mj3k_4u{xjllrOC^JB z<_b6K!zO*Ksq{R@L~IcM>Z(L|qNyQA_VMu@i<&o26Y`nbq_SsnBkLbP%XG&Jk|25C z?LoD@oUdevcoILA<0s*r}PJ%8QAF-cZX(kzQ*6Y86RP4+K@Ev zmg`%8MgCZ!dPK(i;AJ63Dy7T)oaE$lI*D(^oE`~#da31zI&g!kIji$h;;z}5&LicA zcz}6h2>Cd_fCRa1b^Z8lT$t#~w9K4k-4`EdzC=#Ar5-QUx`3K&>n>yd=zgw6J3mN& z456FM)%gc7=$Axy{S_g~-Nm0*gP7+$FlH}1Ms-I6}O3i%bk zppy%Y<5tknEo^T0Ui~e|Pc$jh#ea?Ua)*o?E^!kgJMCNMi{-f|^#%AF+3=6+g(K{P zQ%>f<-1H^kU88~4h#^hUI3>@*ggDVt)$S;d6T#rTyfGH@irHR$d{^=wj8kHXZI|z0 zOXr$F#Op>_EMv@szV4?4Bp9bPw%3$Xi0aj%pHs-(V-{_@5-9I*gM;8%4JUQH;B?Mq zjxT+!{!fMW85?tDdC?~188uvfFPNYrb7KFPh$dGbfQZYAQc5Lq zXci=n_yJmdA7#|R?-O&R$s{EtMxHIvMN_CGAtJxkWgKK)l)V7z@BAhI18?#Qm*Q){ zAAm8OuwHM--X!#JL|18l`5C=SDVt-#k(&r020n~F(f$a?IO zMe|l-ONDtw`s*IpK+8Modf58DM?;TJx$)QFM?wSQTg~ZxGU^M_TMX__R|_F422IkQ zHe>9Gb-e}Oak>iI4=D=_ej10CzHx7_kHF}(n3#q?z8ilG(HYkMxKt2DGJUb-LcR85 zd%PuI9MdFO%l$ZOsk8QqeV^IWqp{HHWUfG~(xIOn_}W(h07Q-Tmyi^R=IAxK8$H4k z2Dy1cdm}|O|E=7J#{yp0x_!= z3m+p&*DnP1d}=usuO!$lY*DK*VbhI*;I8?XhaIXJ04-MAV1X;w&)qH+=a}7WP|1MN z+J`rkk8AYD9>pZD1tgkeTqxE3IAQww*}c`qd*^;#i%WjB_+Y8=Y`x5T2|hRm&OFJ^ zW)zwSfph{5u3ye-I4@<9IBXr_{1CKl8&J>XIXE;6;3RsU>d09i>kii21quBQq@F5s zbP+;YoTPSCbW~Yz(X1Y|@XN6N<}tlZuJo1wQBgPgrkBpKWzogdzGVsbZPK?8OnO!^(J3j`X=uK-{Y6G67;hVs%GcE~-ct6~?<&eL=oBWz ziIaVAUKVeG@` zpq-UX))idb&+>{}@~8?sP3r1fa+!rL@pZ;Shf;9y1qAS)OSIiy=r14VWw}Y_=ud@O zL_YOTEtjn-@NJqHt8B1Qm?>4N$yu*_V_)hJfRuOx9bkAn--D{d21Me|1j5-UMExU*0kige`Zq$ zQBHL4NGJEzP#Bbj~{a!N{K!u+qZ03wm8L&=*FA=k0ri4@|y9)~%DM{q_xUG0xs z9!AY*3S{mKx_nm8Y8LFuW6kc7BKj*lLDeA7EeIjYP|iwQ(3 zB;_rwKUwP6afAa;!jl{CE-ij9wr-9eX9qs%C7<%Fix9pkvnE-m7RWtLcU#pNLxX%D zp&H!7xAm`gbqX_}hDs0fKp_dmY@TE@>r&G$85lILU1{MUw3fcYZW1y~zw~*r<#}&I-GgH&Rx)1kPTIRO3RcnT|-lz zo0dB+JO%qlH?4@{5$CmQUWEyya>6u{aS*EFDThgf>B)jr8$t*wv|jvrJoDriua-pk`FV%#|Z-aY`f~ zI3Q}O$z)vGS;-`KvN)i7b(m@_kR@52VwTd;Sg%dwf0%;k>(^{=Osu`@Z;gQEam29gyI)HnOBr9K1-tTTAH)>kq?UtNP zwF~~PVwdjoj^Py3JrcE05R3q^2tx{TGYZ;5&t0u+L$PB)kb#C-P>p_&8*p^}ih#%hC@3C{8{vx_DE*}@;)w}13)+HkdCbmpe z;$8A~VuKe*iZ1MDT80LhL(f_cl57q0Ki7t?}}>Ud3=*2D|{_1Sp+GCe`>Dd8$+6+>jtL~ zyWa$bn82vlKn}(Spa~WnJ-A1+L}4vF*uUCLorVKM=CNPP#!2$i0Yks7!Z7GqMI4jC z#>U8e$ieQp(gl3G=9nD_o@lcHC_}utK1-q2Pv*9_55f#Q0w|AEkL$}E{4Jh6*%M8* z)t~I|tP8EVC+7-~&eYw-Ti?5*m9Ra?{83YqRnHSJ<-`dqWku{4M$9$RQE>5udOqJH zJKbM`MQD3Lj@n=TG9AkW04YM8CzT+D)J#P)BLPZ7>+l) zKZ0PvSZ>4&{H&0tH5`~;1ORl^@wdsKcxdaV`Z?xa>C203MvXS2s)j8Pt>3xHX`CYE zC`1_?Eb>iLrHY_{!sidb$!UTt%ggcSYHnp3f`?+5gIemceHw+_HaP1_FMhPAH|e`v zc}m_t9rn`wQPgW+MQqT&4?rlTN|!{4IiE7=kEgo}k7~#z4V6D`eH0+*=k6qer?2ov z>9urc?~x>yW)^YGYPGK#fl_6V-#kr`yHz{>H!5$pTV0B0lsuQBCrL#-uw-q;#0|(D zUEp&sB$tzuVx-G9fbPd%xYC>fZLO(8w7;a;3tIP5qCc1rqxB}%KS78=lma|{^c1K` z5?pcHRVEH+4vE9?KsCmhyc+!p;9Z>!a;b`UdB+p8GvqaIdM=HdwM#FeUjSGJ0Dx1p zPk4^V@`ty;v4YlVB#=CH?T4?jY3sm(b9i^eNB3F2r2kv>y#K|LyhckhY>CyQ9f5BD<^a}C^cpjQ%RyEU2v#IwK=4@LD&V;@lJ9+ulWWx{S z2Kb!?uNd<-ipAAijoalr|DdhktNNjf6H67$ZRYl{6|*Q(IH{TvAN(pL$}rG*r;Ypi zo%4sk*v{Q&dGAqcr;VyP2yv-Jc|wgRTH6yAd8rDZKI)ym#eVy3bUSY@xR~#5)A};T z+RB)(>N%~V$U~Ob4Eht`A$x~MZgV{M`*3gmlaOAYaI|m$>xK1+d3_!WR4XS6druw} zD+PMZC4rrk6yQ{)WwJ*QF-3ZLUMf+YFfcHSdPW`O#u%2kQ+49c(|>J#x=SEoXR{JRHJuF0U z?vhI6x4BK7%WWJ9Eg@#v#E&Kg=AkxoiCxN{TO0k`Qf&Gx$keRke&G_5EODsEoVC1L zMt?R7>t2q#`(UV-9;@_H;)Blj^h7zkud_Yr)`}?W0I!zy+JtifYu9j5MyqAqf_uG7 z5;{vqO~)t;Y0z4M8<&*&II;82)&aM-cUHu}hX<}`Nbm0iPRGIPv7#B*G~P;JXgPzN z5?aBT=RN@~0U*KqNXVC=FctE_&}El}koi)jN*AnL#@^B{VBvP%SzKjZX}7z%F0)@JFO_x;8fuacL{5S+`9gyL9DD z=?1dpsxPgUx=rE+Qg!bySULBRyeVDer}=k*^yjgEFs1~cAU-DFRd&npaV7?~K( z7&dfv_yrFH@xNQE$`t7RSYd=1MexHx9OCnkXFQy2-UC4!E@h2eXqpR$twugoo9k2r>SDiUhvq+2>u6<$;O9C zU^<=T4H#>V&7LrGS7+L^ZSsvyUXy`-SSZWE4qqmrW_ZiFswEisWT7Q5^^_%=ZD-Q% zy`T++WO=+@^8EQb*4_J&|2Te^MRwxi?Qmo14diEQNId7Mu>bKPC}{hj#}P%wdjAJ; zZ|7(Wq34D>&i>}=e#W_ZTb|mKQ5}OAamr>?)mMues!DEVX)J=s7yH#DrHY7r#6G;? z#E)p9zIJ^4@aQ58{ixOwEbYkKL4j$lHNDoQOIELxowumJnOU7@h!Q*h5^|>ZEV8qt zg?ICQRDpxQT>rUQpfk*es|%a>WnL4+u;^`W366YtRGcV@ zWxFAR^1(H=?Gsq47`hBrxJ+l$K4b4`df|lDPVwg|?2PzJM{HeBvUEb6mvvSaBz6!Q zQ-b{1U0_LlbIU#BbbS0KNKTf;ED4Dg-AvB{qpRJl9esPNw-koK?0z|Wcjt(n5Iad| zx-KN4DbEDSipY2xzIB-uGba4zox+N&ONg>xVD{wLl_#x93X;lo_#)K(j6NQJ5lQ9b zAAI-yyv1ZBCY7bxlI(G#e7(IR1`#>wd?VCyjsxTeO&pIi=*M&R?^KgGdi^}e`Ad|T z%7Whyua~_B1G1W^>-o3T))m?xXgjLZ`xL9w?L{=IE;SSUd=tROXAE)2!q&dcM*-(< z#TF&+wlbI2-mN6`cy>O#(M?&UOEt_bDY#XF7g*YX-Wd zD;|Vzp}XY+S!1~;Y%vbcP_&jAUpnM28N@C-g&aN&RgjM6qJ!`8$M}9(P=6z>`H}SJ z;!Al*tJ9A0G>d=}@A08YuBl+vjG-cNS6pu?hANNE`$xOyqox~_W-r)!^vIWgelC65 zpU_LQuy)~$sht~d^5W~X4)?2et^ml_k@2}q1vEQVcg$qV-EW5UJe&i&2;Vw$SoNIc zc9&g_abkMd!-j0_$r_xr-Kl_1e~ld}>(a-IAwA*%=1H7^x=0RKyu^K1CA(n?F$tmq zytXUJkMFJ}ynaiU(j0_RFfC!b&?=02aUt&AkToUk!J+l(3oOtnx%V@?$Ov=2i6aU9 z+E9oZhZ{(k^idEbzX!pb4Flms%I6Yy>V0zHoY|a(?Qh=DDS4+8009fOIoc*8=;4p{ zz*pmRJ|MK&PXED5cA`gGg6uQSPC_qBs80R>d0ROSg=%&@<8?rC2kS4GVg%_=txfI zh*M~r@m%kb@4J#mu*zBI=K4c{+pvbo_!m@lap!2E!uDL8yCd)TE5k@lpRzES8$i0G zh}YGt1eq+~YcC$OPWEt0j{VY4pYkZ*&&rG604X|c>^XJP1S`Ci|FKF8JTgrlYJ$fq zkWXLJ9$oB$7M?N*Tr5|6)2G+1To(~LIam2j^z^iiYgXp+Oy_CpjHOLbTkA5-KrqxG ztTYnU@HdOiuGC?sJ`I=0#+s4+I{}AB1G_qTRI0@oPJFz}y{MYFJV(5>PH&X0}; z9V8~EC;I)3K54&caAPgHrvCtsoy}uS)$i}&nja67B_>%fCgjD3FPg}cK6WOWJ}T9O z;6*=U(>~_{M}{n(OXRer9N**@@G2nAGA2d=8VcSmERVCG4`1q%uSZiM%c|?6IlVVd z$m$veKWZ``d-SC+_y%wLwH9goz+$bs*zzC1YR&;+@oYD1J|G0H$AuR?5PKxfwrOy3 za>>VATc&fHB#t!Z<32yH<5`PeI`z#q`2(h5+l!rzV@3k);Wm1hB7792ps!vOI1Zed?{ZvFi?(^xEr?NQRH?5(z zcKQgx{n-X2C3kzlRWq~a_h_S!hRL;`hCKizt^ zrdduPd-~k%qrBgshMg2yy0()9n+CV8YdqU(vPeGxx9eQagvxhiY7fmF#M-!xJL z-w=JIr59n?9v45vs9x5=5WuJyb~QOkxT2FTJmFrjh1W3~ zG1heoZKf=k>xgEYd-@TmXPXUTpzyoNWsKy>b%z|eQY)F;@AgA8zolJtEPl44L2Fc~ z^-L1=jeq@+U!_6;&>qsO|7U&`AZkZr+*CN)ldE-DP{7gPGdSg7M>9QI3~{uh%YUbv@xh_qP!}tA zUo7Wdm|B|t;4Vf|kU3NPjP+>Z(MEi(MZM&hNZr(yAlAWqk7Kb=5eid>t!JM?>5XyE zEyZl+OioGN6JKF2kCNck`JaAMUU>gnCf}@QjJI_+r%3QDfT)tJdYE+|Id13hPMvUY zX{^mqz+QZ7P~@QAmnQMyy%IpVq}#BboG37!@K{zdf5o4v>#FI>LHDMBwqO??mJ~^R za3MvDDX3unL#y;yjBIQ^A#x4&$%FN%y8z_RoDquHWYB+JiD5cKumi@GHW2M~%pQ^U>6&Wai(`x~Mek-h~_{|8TUoDiq>uT9JS$rke zP?3W-xcTYF!#L@HZ~*P6=gc)t$8>)+f{kORTAgQ7fA2K1siC&f-G%UA-S8x%>ji-F zo9j?KNuiX9sf*WT{hO=Lj1(eH_tS$ zgoI6d^^5qgM*5{|GLy>y@c4W~ZN}FJeMJuE>iBs70Lx1=I-_tYN5i6ee18VCee#eD z`hEsI5Z&H9P8{c+En`mmlB+?KVOn=4RVI9NocXFV{DF++0?c<34WGi*&aCrN`=j;Odf)?*#E{$cS42z8xkr|jr}*Xfvi;he!D0H8lM9l zsKZjO9x%e>7Q@W(MEHc0C?yV}T|Q+AK27hMETw%s>S(upHLmOTL0az6b^?<++yI_C zX|O6t=M`2?Y(|%?+2jM}FR032+RrTp=xML`*6@U|4i1f8xbx-8zb zirR8!8wbA~2HM}>?bmbY=tK5}weDnto6$?J zCv1k5@+|Z4zg|JWyZO6cng@=U%8mU`20I;Yb!dI$%!4R7S6i3nvMTxICa<45s7&iB z4DZfX%xoTK3%KLdm_fH&=3#D(Dp#YRIsML#O7kw1*=7+8| zI(Joqe0bWynEU+rg!`PRls{$e(?~1PkxiZMX=-n!1sC~1jwc5)ibC{^K@Pru<1yvG z@%Vp*TL9(nftP>6G1}j7{IJ{kzYAJfjt(j&1QQjLfr^UC3i}BvpsX&=r96h-_mPhT z7$u&`o(K3JMigswtXfQHiw@WnN6mP09drulQLYr?ik#|oQjRskZX+v_uc zUE54h2~5kS1|00st_S0KGt8pQCuCg8nuC)3W=QswRCFHhM2BAQwGRW+JU9vSU-$CZ zwG7D_$5i)(7xHQVD^v;4WzugHs7>v(-k^pJg!8m{)OZZMZfxgwS@-r~u*JIf{<(U( zbp?YNh5IHO6|!d14O&3iWD&A_glD6{aOHQY`TL2DKZtvVr3o=0qsu(gq;SA@MNayy zl`z~9Y7w;dAy1{m=}x7af6@rx(?xmFu40!11=z%0u`69;B~OjR zk%^qvRC^Ep>Vk!zUVe;H7GWwiarn;PQS*{ap`AtT#Xpjh%Yzt_5>|+uP`IC(9R?4( zPRbVmpa}jV(S2k`kfTf-ZN7^c5j28Yx1G*|4FDMXb!c7THe}*>A0Ue7j0V-}#X@ZY za_uP(f3|ZU0=>}C4+!v?%un)7Q#->oG_%=r7R1B;nOQo5V}GhbD6>xJFB4BqHl-Y)w@emqlP<$6s>;b( zhh&$%JO0}H$?lHIn)7v^)CNOBgK>?KAj3k$_+eVSzn^%#jo?4vBYQ7k4Hcc`;}R${ ztP|GxM|R3lfh}7+&3)wdl^=rf;;}#?Rj%oQS^Gm6-54bAOX1^`z52+iUA|guzM9L! z`DIY!Nc3$)%){G%(hOmolboqd%1U5CKjVGAG`O-Nw@VER0D!=6jbfMrR)OU&-f3_V zXRp<*YjmnnBlCr-(^UbjFhVNS7MAV;xvH?^?J@np^m4^G)`M|jjl6SZ z4Cap*2yAz@YT~>FA}X_qa^`hSIN@Ov_{ytHP;rJh;-GX4Ha084P*ILQ<}pnSsZ=4* zmsm+Kmco47SJ=#Gm{YN6;j{J@&cU9=MMcSZa54mcp@Amj^5me5KeKe=(NwKpg<}o@ zoRzY&d4IFO0Yhk6vxcQjH%J3>Xb7Kb(z^DNy#`=4I;u=J+pPzz@_eGX+B3^y_Qy$2 z_1VN&uW7(1|dJ8A&h+Ny*5W_yuGb zo(c*v^MPcwS%hC$v&wnvVCh=@*ZDdWY?KqwubtjDrWYVuvx;y2Qu<-_!bsRSLi|!g zH|~;0FO6#)q_N^Uj^Mm>SM6TW`B(b?H~v3zNX?`klJ0KF5>d3UUA?V;{3|W1Dam-d z2`=N->EsSm*7mhO%i+rLz)v9jSamSxXz#K>PB`N3_nn*ZNHr^)sw6hwJwW#laF0uW zD2!OnDGvq9R&Zs#fJSZYh3x*`e&hXhw`3vSC-HKVU)yZGueQ&7G7`T2iT|B?M|{#B*>2k0N^|JnQp=&$rI?*AzJt^cn9b$S0{bm~wYsQe4{ zk5z-gy1WIABPfesd0j^4Oj@gyyUPOunCpKDYz!X&99SCBj`7 z(ifNgd49>&7*_dtQPafX% zYv*BeoonD`6Szpr^4ppAj>uahf#a+#edI}*K-aoUqVQ0tTpWRsV3!kT(n@Zz~ z6A#1_ zTfw>(A_(Jp>Sf4zPxarQN{7svo|rb+L9slc1=m+-dV*4O7SC5J_Sbq(hYyA);U zZiCVox$c^rwt##q>sEP$Vv?iboe5ad~Raq>j{i5?W@6uNvW*no}@BO}QEG;d4XWyOYu@5?$aI?XFYHlEQ z)qb*krXTIflxp63KMaA}H@D52mAM3fGx!EWL931?k-OPu=$ymfg_dSkbXy^&p&lF= zgCYJf`7Day;O%&WF0lAcy$KePtHDUtwgUX!V%mY>H#xL{^CMw*rZ@iJ^nN0ZObHW8 zzww5{tj5*VniWfywp<~TIvDLw3TG8{j+Y1pq{5KXXWsqTn~O0cUrQb{6t&;3%|*!a z)6BPqZYS9_kk>Zk(8W_f@0-C0eFiEvRBfsO_s=d z7~=VCzl^UG>@1SQ9fWO*JcGB~tBosiz!4nXik1F6=8rdxrfLJ}KPChkne+>+YDsn) z+&Cyq)j32`mR!iYvM0iv1bNNIOJaD+oS(ScaBjQ^^80lLTjWViE?>qoiY@g1Vu8 zPi}3K`$*XGAGiHP<)WDv=4JOb2&1hd^RS;eIpsa&aZ8F~Qv*v!c>{8cPnZK&4!ktn zA5p_O7b%jkg+_yTYz(qK(RKzZuBb)2z9QUkmc~_2a`-7)e^=l0*o9=Z$nXo7g44oE zc!yB8FfIwDfcBUXc)}NQzwo_SaGXBwc<-80qR|w1I2iH{qDPbnGrN5ZInKW3I$U5d zT@2y7z^Z2eUtoa_ZmYYV2{n1pWfb;sND+)oTBQH2fOWuE<<|!)M>D>2g%Q3EGx`&m z{-BQVSHYJp5w^j^r7IfH)sb}0-aKzkPR(F0!4580s7pXYw@OgSs`&tusX6)Su(oa2 z@zt&GwHB1EUS(XZ+EQr@4OFknGbA{Kxn|NA9^h29EHeL_Xa4np*vGw@nFZ)} z$&xev|4{#L&3~r$U)^E{5-ieAHzsbE2P)MP&jgg~7-u~hdLa7>w>}$HoI2 zmHit{TZ^>9qNa4O7Kx`)U{&jXz5`UZ=lau>GoAN1l_o|W! zP*vXQ(XcyNmhTgp4^q@IebivKHHtl{i7{G48 zuCpm2W>O%>E)-T0ZN|#c%(%$dLi9Bc)Vu}}o%H%CX}I#92AKw2dyB~onzYOtID#|# z+E^7^?Oy%r`LGqYSb4y%G=y2dSBNj7uB>qVrJg!S=P)~+L1#fEfy3h&>C298vQ1C+ z$yZM17ASG&fjRYLL0aCg_Jnw!VZ4eTIb7H7=w^6i+SO0C<;iYg@#uUZ!{dzI+yZZ^ z@Ea9HU_`uOVmDkk$f2R@$aUj1I4*mKQ|M4NR>#ST<@4Z9VAnL~=CY|V*!LNd^U!eL zvm|8e>#c~QYSOWqWFA9yO&$zT#lSD8u|0i$4JE88NmeH}vsPI#ea+FYTEleH_MCMZ^gl~ ziGc*pc{J#LyM!(h@PA|>O5)hW&MlwtA=#%yOVzqg{SJ^Zij{V_WDsAIDnOrn|8YwD zqdw`27@5B(nsaP>RVg?KQ+bU$ZMPPAx+dGK^vxm4@!M`e1L7GIbkCji=XlAcFxaL4 z<&sxPpLc1h>FWW$pi{h0@g$ekkFQR+=b*LO3ymiIZbDM3`-{#ujiCNMFM$Icx@u1} z?~j&f!m}2ZewOayO=3z@SM;F7IuFn|wlin8@J*}iCRS!Q&3$0?>}lqwbbBANL_u`f zL5<^bpQZAJy9&si#zPRZwxU{9e%g>*fHrE+?MYStb^3kfhC%!LAKYh#AL8?$6jcg{ zg?gye?WG0z4`Hv+&V6V%wZa;L#SFK7mfpX}^!H;m(lc4b7D>mL;F)7SKR_Pp_bjWl zW4KYhcG7+kDV)gy$&I91MNP5GP%N)h@3MN)C4;$*soItf-fZ1fUFFwd-QiKMJ%SK- z2iH)(jf=BaTsAu1Aiv{pNJv@SZI)v=x7ox^ISYF(D#~m%Q>CyI#pRwtB2@C7FujZP zF&X`w{Pyjm@zDKn1tGfx_O?l8;5`k9PU2laF!FW5Cx}Hw4vH-#uPCuRAE_hc7~PeY zb#k*=8ajQ!Mf>G8=~4%Oi!8C5+&XSnXs@?({n-oG-U2q>8S*bEt51IFMqKb|`FT^o z)vRY~ahkeZalhAPP0(Vg^j=a+s3eGZuc~2I?7tRr;m1%<)FfrI%T9gu9G49FY4gxn zF`{0f;?sN}^G|cxfaX9vjw!vvD2}V9OI-RuFuLL`bF%zEbbyobT3$zJi&Y8z8v9DFaCd z@7b9A8PF_Qi{JUY(TSJ^anSN1XcBQx_1?QZQps|Pde-l;!URdl_KiuR#?+V`>8H?Q zKE2gNV}J#}PV>$L6?&Ow_S;kJD$%OPdnKvWe9c4u*`ycV6wcCWZ$9WC1M>7>iBz;2 zBjB9TBC5<;Y~A_9D~tCB@J8=_Ro^3@m+WT=zP}CaYSif^JUO=ygSqDHno-=fTi}Zf zdVMcjFFRBJ16bd{aD9dI?^z>eWa;cS!ug_Q(T8j$NsM8^86Q+xeB+*y6@B2M38b1_ zZ_C$yxjJK%yA79K=AvfOUh$Qf!DD*!J`nlRF$%*+?PrAANk7}JF#g--xjp1>c%+}1^xh3x7V?p_qXQ5)WZcS z&e?(k7mw&)!sVD2PttUb#w;yV!(%Yi)r8qA2qQl`S4|KoBm^&}i3)8{RzA^Ze(cOf z#vGG8I)?KhJR8hh6dBqwbK=ACT%AZD?W;3)^jQdT39UG8;+Q#9ma6@6TnF%z%>0d; z@SeR&U%t_5j>mv~8@~lH{~usi-eydZKlNpW5o(#!2dJ4tFW2_@8&eJX%0fNb1%g`Q zVtKvQhCb$gweVE}1zX5-vYS)=3Lz(hh-U(WCgW}WpBneb5ES@2j8;AdSxbu!aXA}U zf$v$@ZC*(ltV=Xc2DnNZ{sHKd*mWEAhjcE7iiLFKtQw_7L`@vgNt6YUgKW%R0H?@v zW^(m-O1I6Z;-1!7W>eE4n30Tog^BX4dH1pL<>}6^p%?pmC<$U32;!V*%(HWvJbIS$ zHTi9uPR9VJimz|zkWZl93QthaeJ%K1`|%pWxv1<0-aNL*KlO!tk${KGkhmIfKgMC!wD zD>KXTl`TZww1u6yOeyQ~-@0Tk@Td~V8L$VH8L*D(5q2V2qN>YcV`a?9m)npSR!Ui0 zcapdq)TQ#rc>c3^ff2#U)sktCM9r|XTCT5*q<$!J4vVwa(Oa29) zy*$oIv3EaDN2ZT7*S3K~q(g24za?0JL1^KTD3!vU3>cTNjqoJ+L|RQu!*N6X`Pj>9 zo4#bLB_^x@@N*$frZw$|eC;2#FHHsmhs%aft)%bBqPk$3{#=<^$BIl7^Wb#q#usGj z5u1IWV@Pu2dTMP$L%kNV87=FLh z#2?nU%kgd)CoXeOQ`)J@JX?pLWD}*m;1Y{#|jSjPkt^+ z8;{h1d3j#(x2HaOK#4xbq}-EazuFQx_kzrMfu?(u$M}3W6ipa*6USi=TU^I?zptS~ zsYAYj4vZ`{R>gRZ_!`4onhovkuk8zy$=dPOKo)8iCIenNhd5%|O!Sc-@UoF^rS5Q+ zlZIRD%9ai+yBqQa&H=I7y+{psZiyVd*XEnYaT8pDv=e&)xP`$jPsrUBR{-My&t znX!n~aJ06GA`|}8?jFxjThw0X-`;BH8j-6^`E$*t-`@&Gu)KZDO(Fi)f@WpO_2+?! zHHigFZ@MS7K|EhXJDL0&qH$91QW-RT~;1l#=(k#WWL@jqmYp=I1 zal_S`)IZh=Q+l(j_RN2`@mPlQe&d|~xE2)NN?+k7^WG;5Yka=X(Ij`0F6}XNe z1n?&ER(Bg;dR1+Yg~n^Ej&(a)6WYCS&YfFn88M+ak0_9tJ5;RIowKyD}dW(Pk11QWs zr+fbO#z)c_lK2pJ>_M-7sU@yWTMT1VN3bpI6tX2u-9=gQ3y=9K+`FK{^q#dkb^p()pE^(`}vlx34B}ym+>q-b|jGVT?A% zlj4x>i+Itu-ewlf{MdE-D{0inM9A>keP+|qdR#YSy9`erwpgWsGf;14)dFWO>Eak*$OzD~#H08^B zQMXu)xtbiMcG9O#dmx6*R4QZPn5(6No=FS@1oejqg@YV9aLdQMtrN0a6prCM)9+#0 zr-ITpV_`0g1G!i0j6M^SIQT7V(AGA5b$2sejx%vVaU6k*|9guO4M&OIBVcM`-4H#Y zHlWBSYr{PYsk1_Z5$`tkcV5Pka3&evV3q-J5z2rt}AJCCLq$VEyHxsYjQ+(`cbcymQP z>?Olh#d^AJ7}1pF92d&_E08+uX<Bcbop<%NSZF(X8c1Y^N)!~x3%cJu9trc&n=Eotdbb$;nt2J1q4cy_7 zmkVq-l>y#KbUVbNFPXtTU)$c;I7%H2!wGbTnX&fG=H}s~=osxMtn&;iuztUCar5Qs zq1O2?)WphX?s?(2gl@JnrTlLsA?_{Ru|iJCQ_Fh%L~}vz+}li;NTQ0Li>Gd22j%~Z zvA2q9E9$yHDFuobFIp(>6i9HVxD=NJcMGmX+G2&^p;&O&K(XLfqE8~u_9x~P$ClA@@tTi+Dnse92^7#f7{6pJvSu&uIvSfxbh0-Xmm@0};2?iEA ziPf7ODo|4P@wZRXwO)&9rmmhfFcM~bP;7shJ?wOn(GQk!buIX0qQ*1j5@t}FtTbaL zt~RanrX@C5u12Z0LL@eUTFKwKyJ2jTQRGtU7wVnO;orPz2*QS%jKpz5&3id>O2YJSN=HeiHJO;FIZqMt zgKI5j0BiQ9MLoZQ?JL`3vHp2gjtHgEuLseDhP)4djkw$c7VQ($kvZEsPScPSodjft z+Dl01XAfp)j)umjDP4pYZ--3=``=`@zWF+n3-#$!!HJ|R)k^hESu9P;{ySgn%1JY14SbzX`d21Y;H}29 z)PCdn%cj~G9dHz8b!hi8S-K>vGBJ;ZuS(V zp7#$KcA@VTsG@U7Mo=&3%c<_wURD5r{j74`)Crs>3AS9-Q)*VciJ$2;rHO-HX6 zzxt(8Tz%W4E^s*E5c}v5M7NBZTyW~7T=9=V81|buu8@Me;zOnv7Fin+veqBVGXo{< z_y{cA2pi78PQ=4Le;3CPNAi_&+{Z_}xUsvf#K){fC@>&4wLrnrJjI8V#jW-}L{kIjCF*_)z3BS_Ctc9zBFBPun-$XU z*Gf#ioN9EP`997$P$%J}zpowrx_oHv3+{hD@Q&wGo8^P9Bvw>Ab=-Vw6wTq#Ti6@2 z91Etu^p)=QR2>d!%B-O-fO5quLs zA!YQ|XRsh-hW(Po8>8%mrv5%1M1yXj-)2d^Op#3JoH zH=p??XU~`4Bu>AN5H%iVAA}5x*N-_G4tT5=|E|!qLC9%lH=5KYB%Z%=nGU#*e=JDk za%;=-cqjA^%^5|hOiuP!fe~SiwprMlE+){jie z`N$wK<5(YaG{)ZINF&v=KMjxm96(Ca`A+#1;tx*Bq;Fg5YtZA+b^pgtPgu0S;T zty7ile?Ib+YQi%ycEs=|^iW(rhkm^?FO7-xaf%ZETdlWQnZt90@q)+Gfjc( zl9c6#8r;Yd1!L#JorGf9m%dR|S$CB+R8iW+Lb>XV#B9$E+NEb`XKuLyQ__s;lFKmW zt^~OT)2Jg7TT>JU5H|i{+S#s+qV_7bcs@)oS`r^zgSKjaV%7JnZiC&~*q9Z6uW|om z9MT>fFdYB{9kCD!gRz>O%F`nBfx4RP$$_9Olh8abC+B3#RfeFVt~oKHUHg2mcBowS_{Ftvs! zh8pWzJjJCUEi1lr$C49EpPud1%2*W*tQ9fnv&Im$?JK%$ATleg2&DvPXl#D*ZPq2qG$k#rj$bw1A^tAM#lrP}ETwPz z-zr~{#;UONdA<{(5^55QdTm(jm#~$)Qx{1orf#*W+8pIv`WGY%N;`SK{sa>uwZ>Ka zzaN)2D^CIVJi$U!ZdatIh5}OivM>UP-`I=+7%@<7sQfJ__D1JSznahScZsSoxT3E} z6;YdWVo?GKW#AOLNABm|U?68`H}=sLAz+yn_2_&cfI+-iz)MjiKCtKlE>^xzhjYZM zd-zz7DKf=}4;lid$}AdO4MP@j#m=ZKmK&DH@`__9p@`>`T9cM*qb$f$GZ z>+&&BOdM>4L~sZ)SAuKq;asF1VMXO7Lp(HSresuG)RO#+EAcJ(KC5KM?oB2Dm&dzh|)F!-l-E!5m^bRv~A?0Y+; z9XYn?MMS^w=6)h2foxKcID4p`71NO(eaP4Glzry5y+q63n0ys)4CkN1pz zTa*E!|6XV3(@?RQ^HE>>CJF72(9=2<=;YCMTfKn;APRP<-dgmRMxgM8k6nH2#CKF+ zkqc*-K&Uc_yWC8JsbVFK7oiF;u=!hjLF&6Zx7`DXQ%`pvIvHe4qK>(3Qc`ZSB*=t# zaTp`@#Esec_6n5zu4Lf^MWvk3N;+pr3oC7bF7{PF?jhS;6SN1X0JU-yq#1#TJm-u) z6@YIAC9|hVza_JU8A?L}Hqn}bs5a?X0@NY8VzU%|izY|xCyJqlREkhpHqE$_G0EaU zu)|KIqm6f5kv>w$k{hN(?WH2IM-*!388eXt%dfFhXyr0^j z;v^XYg)l`yW>WyBoem8-HF4+19g5jhH2BJ z*FKo&X^eApRx@Pze??e+lzjc^n4WCXMZ&GST&l)l6m}wpxib>klqcm3Y8F`jYP1|m zy{JgrD8@a`@efTMH$Cv?mbKv_pFmaCBtitTmFmkYOV1-wJG_Oc;EpxOBp$Z1ler}C z($S&jbDTf@DdB`~wL=sAnnNQjA-JI|zdH#r?!LWf?Fc*_L&ntLS3QNMmS>r(_?buj$6X zBcKWAH@@g$i)h;eMfan!0}O1ZEQ~vT=yk*@(H@nmkU4xzP_^@O>01BQRcb!lkszH@ zENUw`64HSY=}{&VH=|0=SWT6>QvVe+b7D8W9q)MYIAOLROHfhL=tDj;*8RmGhn=mS z*x69%>f4gY^l=b*f4y4Sd!Fo$@@dztX&<(>9EsEq>5-{5$SB^%o8qyUHP&<}bDSLLiMMb&Cf-^eBVgP2cLt6cz7Q=d&=wooB% zc@OabftjQGG84J-unL27*Z5RkYUj-{8vxa)FxYQejm;3B3(V_Wx`eg6k{Al4!Q>n> zOL~-swF?Tx%jdDgcRrp{2z%kX@!`19tCFj>LXbV;Mc-vct!Ta=nd2;n`FXdYZvyl& zYQ3!qpX0)ml+P$s^6V`7TRW74Cbx~PQ;rU|7+4;vY>8;}Z7g~0nc)rf&P_|8{&p2h zTirImNzNgBsOl+ZHEjMb@qS}!{?gc4_A&b-==wfqC-2B)9>PxRVNNDHCXgfZ zGNtH-JMILspQ&hSJMc?(b8A{?ycbvu?kBGh+i!lNkk3EoeXn0FN_2V4EzfK6<5E`+ zbU30^knFyOXDsKGoPJ`*zels(luDKDD~j=;r75T=5f-t|b*a?+*PfarL`}IhE9#pP z?tq_oqT#u4W=rh_l`!RE#x=vUPa}5QJ?O?%lS_8y3ny8dyp#R!D4!XZ)d%WuP}>l)(O89&U<(};r- zH`d40SlTD@GcGELX)T8AZOOwY`w1elu4bz(>ONmYyT0ZTnm`~xM&3)z+FWX-3(CWs zk1LZ5kxx?|b??%9ZL!51cp*pIGlkJ5lRPBvRMXVzjIH&jt0tFw6&~WGudy2V_O35$ zKDJbI%4cVz3|_e3MVRthF|gC?D;KQ$Cm}`VvF}Da`+iw5=v2PxKxgFn+lWFRixvMQ=p+saD!Td;j!?96 z6Uq!V0P#H%4@usswJv+?q;h+{GN?6A5UHyXsjJL}_>v<{n#BuR{Yw?&ubwRbJld)ipNUST!r#rtB|LZZk_ImqPnSrr;kS!F4cqdfZYt6mN3 zEI)I2{U$T!yEK|s&&83>l*daN0rJ$s4x0vTsak}a@YFvv zcW1f(c~Me7B!{NNX1!1gq<3jj?Cl2tJ)VO#YQIc#9;5)ekBff!P%#)*CRFD+Qf?G9 zIY-0Tg(w%oG&8rZ+BvH+)n4B;O}_D4Qo}-@h6&KSnf^o{7CieIE{ezW*Nv4*Q0Mcv znZ}Gnw*Wt-!;(>H{dNNG&vF$Dy|=DMT&_+)xMdPtLQQwOY(S+{My`w)=6>>q02)uE z#M`FpkRr{l)4e^IxAg6cp;a;ymeoU!msSmG=SxdyOe%%%_c7@wRaDU+bJVKq1IX;s zC7bz$M}H9Q#27oJs6(U*f=_*a`AcPWtS^FH)Fmvgub@ug&**18INlF#G6mRcR!;T8 z^Wdr~99B^?*5SUbyj5U!^#_(Cf|MGSyr{_iE?s&h1*I8Bw189HM60)xgG{%mNeXus zn%gA5PA3i>kzwI^cqGrM32F0bx`k&k=1gxt^6gZ6Gf(h60b%jq0O`%9DbUAb0*8-N zH3v_6FHe+S?gB{5VCLxgvbW!k$agBVWG!NUaZ<`#a&f|FHg&yZ!}@*S+TBac@q1@U zzV}c2u+3090XkvB=Fl0781TeyX#Y_SUt@-=3sY^HdfQC3xWe{NkLP>8U0l4gi5FbF zXJ;BEIlRFl6m(KtD{-mWdHb(`i!?M2K3t**!3LmUQ7O7HIVt4bCguxS8>%8R7%oYr zvVQ8U%8s~i3{7y^r%gC(oK<}a;_H2=&T#wlY0jm}QT;)g({il*Ap#M5ii1b>Vr$cTB|1~~$u(($Kri}|x z#Vaf&%d8hZR07EZ5LSmHYWcu2tUhP*UeVrA!Di)%1f9;Ca!h3bLG5al=i$j{pN8N* z1pSWD_@Zzfkg?~SKs1{;!)S|BGo7ehe8pFc>QOY@3{hbwNQalRD_1OxSbg%ddNG*% z5?_BKYEfjCJ!UP#i|oy;!4_QMAKI&~sSDM#%dN6a$6{sJu0|8|h>D{3y|5002CwM0 zLWK8{C<1o?>HFFadA9NcLNKM|{BU*W{Qhqb0`Jb}uk$~&W6^%;E1g;;{5odCtr7C^ zTnu51#+e^cX$(QoR$ho?OwtikBXxW~4`fk%kO2&WfDU?>=Kj`99; z5W>Ewq3QmcyT6z{R6P2{2l&wpQ73jw-$q@n7Pnn)FrcS^4or( zz;oSZ^sM7?vOH)}vUeE!L^xK#o`y$Sst7|oJ4V9#R&bI^S`XAaS`{|j2XDZh3e47% z(_n7H99h+(GeLJ8h+)zK?N{0}eCl>$5QEzVqcK1liCgmH^I%}<|KnCN z{Hp~5Bu}c~C9$cfVr?Ay+oQq6d>)b7Bfq8p-Ft13WjWdGr0!UnSX2fla_zDbpiKi^ z50UaOe+7{+$7sQRh5gQK%IDIxZ0GU2w(VD!NUY!OYHp>mx+4(#!JIR2s23FYbM0R1 z&iOfOtf^AJY6J^on(v?Ig855ix-MR=DylU|m0KYBS>1YQnrm6JM64r>6V+_|YFUS^ z?b*84_ZqZsb$|Fb3gsLXU-;)6eAJbc+fb=B6tKJ&4By9!!~W=_oV)#bmx4GWTQHbr zG3*WOtvPcWd#_?I(2v_`L@ieL6Rnm7^CF!n#%73Y(2E6w(o(iocRpsvqb*TdnF19j z|2sBJW%{?T;gv-&A3`S;^u8Ha^pZX1zxE8O{hwaGVZ#sTt}HgUdb3fQM1oOdE5J}z zlQPYt*0Rk7ip0t-vNE58HQf8OP4gVB{z}#Vt;Tu5lJbFMpaaL&EWEBp&e$-YyYhcs z82-OspuS*;!kFhs5(0Foi4o};5qS{6#m*Y&k8RLHKYK!>| z4{)XT?%gL366PRFJo8f}d!s*;f2yrC!{jU_z&^MW^h`XzSNIqEpdUrqvI;=kxFtEa zJRDZ7QJu*%H-!a%BZJ}FdsKE)R#2cKDq*KLyJ_bpAy2M#jJ!ziod}Gi!gwf5|-f9m#w5#@KfRo8wiJ;&rh| z4#VevIFA{hKmkvh3lRN+XT_-vGG$emnBSd(^{g0Q?-;sTgc@xCz#Aj z*>|>SerPkJpHa`|yeJ0lX9{+#F1d5FluAip)gAeT zOm))uaPSWeGF>NB?C4Av*(A2(XlR&owlhmDIK*dPqho!c|K*_3(bv(wbhxtSADTJ5 z=+J+|Y8Ym-hPy$BDoSaeKe zi3ztZx0EW6=PH&+B3GXNu958G??s z1TcaAhjJ!kD~Ht)9&8I&1V}B@IbAc!YC?g^lo$Kk9#4M5;pHO#9cS<5BEn6fKV^K( z|6|Qh9encRsZ@TtP?}|!QJde(GQGSAr=_%I&~p@6mbcWCp($Ctb+%FA@Tk&>SU{oF zc%S;iK7~^KqVuW=2rrvngW9OQX$poh31lo;QUi~O8??5h(q!bLpIN@=>Y6z>e;^}D z)PVHk_A^f^dL}vxmhSKT&;_+k7Kd>|3HOJ7kP!6gMhFC!$`S4>u`lHL5p?IT8BW37 zLLG?i^|!ay&_Xc$_tZKen)GY(sYDiBw=zSY;@Pgkhh9=)Gg&vX*k#1b=6dT z>vUzYqyA+D)>e)oD~2T6WXxeywt18J%;e}Q#=!<|u;8vefp)Xe%E0;cAKFpxqfVYu zOnyVqLtp-`Ugf7sesm#+G)S_1h< zlnh#dnpyR*HS~{WhS)K4tc5{|4-?$|DBF)0jz2`{zGd9=06!^cOe4r z9xmVZ*7$U6A-gMnRB%s7HXc2#T|Bjrg*{E>bgD)vc)`?ob!o)S1HyPP!9C=jd=;U? z96A=LzN-tr2}q`++}oLbSe!QleVHBVs%VBsdEqd^6q zb5e>>**5K&;)N)TQFS6_)qwMx{DyJHb(>J8&pK6;xkxH+kN+2eEI+F*(R}#hl^HyH%a62y%(!OmV*5wn);*eGLYyl@ zUpW$l3T+^^m2K^OpOx^?441C_8J2rXvYO#5udDQD#!B-c^5$u(xP|Rtu$JY|QpxX) zCy36u_}t5jmTAPL=s<<+j#g(d6uZG6zD(8;PHZ}wK=I|3`X}!|<-%(^3P31ic2Q5* z;H@e{U8tIhb~|jDsNsvkrzNT>jY;d&MXvH7_{>0$ec>}^0^o60DpFfrn)Lq!B!9&o z8#n)r#b>N{V8J=Ok?s}x(Vy!uO)wwJKER>p8y7Yf6ESbmw#gO0Z}kt zKg^2Pt`I0Ol()ub&UAWq(T$y>!n|fWP{?FUAJM`z=TD-Wd(UZiV?<`JLXtb`zDsj? zRUE>!1l6&8XXm`ss{(|^mul7NE2k(x!TsM8S90a?pcFbP9^Y2L?tv%3jL^Ft-GY#TMts#^6)Q!sMLnFsajzq#&fYY~cR4d;_aPA=oD2icfddsT6 zUuTE$VrE&fhy4^1RH5pZY z#5%Ja(SMP~uKzm-YQA)i8Dk=}?icwG4t*Qb`w{wQ{~sC$Nq=@%_(V-3=N2hAfy_91 z2)%Rp$4({_R8x^qkPu^OcdB17`C4E>6J^$pw#|H;n{}9?<;{z=`|Hg%y6#_qz&NGYov@VdGJFrmGt8G= zaJ1TP1492DHB1$=Qu{0~Ik>8>1~&a=Y;mwg3rb{aEQY(VGk9&$kyX1w-1uLf8zG@ZS!JA4Xdyzkz%MQTaJ%FSglf9mV32`F{#e*JIY7Q+mLFwM>M8F%x#K9KQ+lG z5RinWDBpNMA9i9Ma?<2QZDJqIyh-jT{pY+OL$|6y`i-BSllx(EMFQ!K5)-SQ2S3B0Lc48e$J^li?o zD(=XhbH-DEa4>RD@~>lFVp6FYRkIh7v_{KswI@gS);cB5#JAS!LB!$LCcL7hx(&o+ zbsXzH+D!!Hqkb>ApnH*}VJHW8ez1aoja89p-m$nB{F~`QiuY8t1b8D?8_B5O@ha1> z^ObYa!n3;ji0eg?hTi~`1DW0PpdBHux0n|-PG`n5X4V$Ru753qHE)`e!`ALR^x zsCD&L5iAtLx3z(a6ybm?LC7DrLhzynHnbI#K7a8pl>>#8+k z_m8HFHjNnxVeiI&pLeGJ5N0<|OJfgY8}D*o;fD)kz4>x=N#)2unF3qlo>mk=KJV%F9QPneRB5#P7_6gba61VIF8HX;$Txz#7a)h3{lyb_f#PupiN0O7&)$ma_ z*dUO{6-W050fmvJoVf;9Zu!OfZoX_SSJM*>?~1_+F+iX-QAeO!vm4_dYW1WQF}c~_ zzYo&)`=lPYCrLS{gk&opSjnk;`}^{wgb=k*OwghpY=q5VO_Y{2dRK;u;ERA=BhHJ@ z6G?VYtXbMZ=cMr-wPUEq5<eSj+I#VG zJQH;7RMn{Q8m#<3oq$p(yWHCBXW4FLtdAlmmw5DsBcncFc7s&D?nfUcF`+Hz=w+dn zRGn-A*vRV-mg!iyHo+Af%h^p`W}3O0DO+e2cIzk=MZ9tv0JrNNU}Q+@m3#+lT78q! zK3%3Xsw3-Duv(rmlv*;q{9-92iK|bCXy3({@EZ9gz~O--RlcqtJJef}AN#7(b=-EF zY;tiVacnY9BzAEN%b89Hds;bllV>!RSkN%J;k?D#K)cFe&U1uIgG>1zTJ$TgXGVCZ zw9U0W0^u&1CC(98(VX+P>ANxS4CoYn1p-s9j$Is(kF2*g5IF@XsuudYjkX~_4gbnx zY*fQ$4I*Ve&E3fSf`;6hi?S+EbBRX&)sCcCc6ijJHH#WmEdP^{Ie~9=;Sz+*>v^lO z;YXmQRlP~ib02`*%9vsA%~vB_H=f2pOa&0*h2QLR8{KfC7XRJA%wkp80M08My71KF zu{S-y;8bmrB`~BBcf5-H>ZhOv8`py*lJovbj9lYxQfJAeHBu>FIiBTFA*DzV{N*Dr zvgZFYDl6F;k$$F69i>hDuek0L21d<@^j14fUag26saIBk^OuPw;4&bEXAQo=a?YxB zaJQ~f%nzP?tydqGPPQn#Tj`?mi{?Ln0vxd9W3~oKM1{>ad2nthP9IKf9T=cZ%(+x( zgrMl1eZzwj9Lx`WZY#%+amsxHG8Z(;l?gcm7XpB z0OsQ+#6Umo#AQ_#J-nj%z-ZT)v!Us|FWL3Y;_i+<(Dr_f5>uV9HONj zK}?}qj%d5wv`w^+Fc)sl4;CS=H*=Y*aEQ*uL~FCF(cn(g>wjpCxVw#X;$^t`*-2i*h5X?CrS$2^R%S}n445<;97j(Gsd z!15WpBr3b5ehEqKQ3EG(WIO)XI^NaMwA7bc@)Hp;vQ*6a!`vS4Qn)TzD;ad?c1{Z^ zL4T42V09dEt=JVT%mxL`jT4+688+lEAeTlA@x+w}bMRHAen9meH4s@TVJK-_?W2~N zS=}LtI>T;KLYQ$!Y*82gPFiUnI4_>8%sj~ahH>XUmWsRhz3`bu9J2u5#%*;t*mv%W zX3jvHRw@bS(|AcUaN*;;lRiSJt1;f`2yPFVNT!)1G}6mL$N5R_1no_Fl*YL^zQ5k+ zyl{B%v#VV_HQ#)K6L8+JWVRh%c}ovJ( zZ&OMgM@Z=A&STiIO=iA2JvUY3jsqZ={-H5a3EY$m8t=r_rubEq?%EFL!3!b**G09* zFT7pMZ1@V;Q!iSM$XBM-v~sSnkcnz#ax!G23ycNN0{B(y?#rX4hLwdmU4?#+jexw4 z{4)p%*{`8&Wa(%#Rj)Q!gh1g>@= zQD)BMop=Mn;G}PvRvynpy|(ybUv;VXAxZgAc364+)PU=3%)Mt<yW>}V?q3e_i$wi~6{&Ji`=r|r!`_!nU2IgQ*S@J39laNwfk;Wt^ zPgTg@AKIPZR8+oNhfKkoMgZs9JyO4dBHdbV@Y_^`#@*S+!Eb+k5gbj%<_6z97H7+G zC413(Kq2{XK!~~%5f{~C@|Q}Rm5aAnnAYu&bd5RdHz67?;V`>DH@t)T^8AEeR5X>Y zGkBWMJ;2g$DVSW*BwmYE)1R%JIWo-;F-3^{BwOtUS>Gn@hD=QA4G4SlglX-nJzQ&!5RzW}x;pC0Eh zTI;3L8OEFghkOtBnj`3)HiI#54HMosBAj)@bIDegxY)3ukpfinYm!F~`-&0tC6tnT zFjRV|JQuyVgEP2xi1(anX`BVL-BZ+rDfu(qy3cO~#kts@N)B94b7Aw{<@Wyl?yba0 z1<5S6V|hTchoRO&zxUz?wfqau$p9Qjb@#D?D5e0Lwn_MGKX2DsEQ#R9d?m}$;^qJH zLxmsTcOopk76almhhH{%^s5gy16;k1vGh1KIDNqAB7IJ1k4pW|q08nW>#Ok(=Q^|n zJSt%<)aoiY%gmJ3=_^$#ET^hVt2k^k$XrhmyZz9G0+3vDU7Vq?z!=y2|G(`^(oq|tf{A|eW#(7GCE8_TVL!#m=G3XEr7r=QNg!K2)5{8$S4Rr3|A3%`KD3l zN{^0E9<-mh z5ubjbVVGf4l6&Lls)8U*`zrXX#s+}S6i?*DM?SaPK%|izqIl_2^1i7B5Yy6K4u&(^ z107!(tnr8=iu0Y4?&<}V+qMQxe+O4H*#-X|KTZF$D>1k8`k60BL*IL2&I|90TLS67 zxeY1ea$z6ervgGnrwqpb);l8zd9 zzz|)<2fO|oN)wk{!9GsrZLI`9<|DJqJAQ{Jd7VRCAN;0(yoH03EhDkwD>W>oZt_`L z%$J&1nEr!Y1F~iUxTlGRv7zlY_4D)#={Dv}>F%V#@#_^M+_+ah8U zibQmrxPp+oL_eCTe?0x1^kLFrS2KJ<2U zUVoMThM;~CEos4KP-2=EHNa&IfjjGZsr4)K%Mc)x=?FLR`ezS@=fL^=1s3n&3I3tg zRYAwt_6=QlB@P~YCob8}pOx%MYQf-y%-^=sp%Y;6C?-eWAZ2 z&IQV;y83pd2Z-x<}?cBh1iyAykeS4nt$b|-WG&!ytm?f*~od~NuwFw zW0zl3DlB}|sML%LXJf+P`b10j)c(-NG1)bZ#`V^8ai<23v|8f9@SXSj|Dnakwr zV8b=b5}+xk79X9c^wukj%_sy~KxN~1lYIhS05rTP}24IhWpQ#Z%YnbPoGQpea z`$y4n)oTWci~iy)GVoKr(eV%IFFAW6g%6*#6zx;2!FzfTWX!Eq6lMR5D~#$&LHu{6 zt{`&M#ZjwVV8e)QbEe0%D`sP6;#ovWxhpXlpf1Fj!|N578WYJHy%i@<4?C{FSzg*r z_oS}y9%y(-6w1Ci31$$6y;oAmX@@z9(N?iL=++?HLrpf?Lp=>CsKHfLb7tDb+?8DB zqb%)dWo-qDJ_~fif@&U2JtgEqsr|3Y^okF`#uJD$6WeB@y{R#kVogf{fPn)@WVk@F zbBdFPs&!7hvJWj{Wz*Q#^9|h3<7Dp3ok5b~qQ?u9$wHxxjxm~&<$q{@E?)ZP2IkC* zQcs75k$A{>^%szT$$5C>_9I@-{Vra+9Q&Xez?iTm9r5vV{p!QSxkjw^&o|bncxbMZ z?$oEJLt|QL6S2}9coN|=$M2>NjmH;6P)g*)%P-p`*{_vf==$JTq4B%4k&syiHt0Wc z#F-@)>)`=}*7rx#5*0yX=;AN2cPanCbU`D~)TLrHr?B^5k!!Uk@=g?QS>THbIcoGH zuBB$mnP)417g0w@r0W+QW9}^yqHm6qZF3Rwl*T%<*thz*%RRPD%e zmXN5jC$6M&@09w7awZDy^JjFaam721VfszMM0IOpaoj;LrJ9lDWH#GaR+F$QFp`G{ zd2;iCP@?Xrvhm^InCrwKb*#fQVU7IyLA06+N-X}o>1cT{&%Xvrw@FnQv*{HF8(bgP zF^N}CFAi#AwV)v*RQ!k;i+gCFy5ZZBKDatc5BTk2kji})Sfj(`oSmnvM^JC-N%TC0 zoQTy{KgS5+a>|tjGuRfSv>exvQ&mbzl(Cvj)U=n_DaeAx8@MH%g4DRRe+tSA$0TI1 z!juc4RR#PZ*8X2el@G6rf1|Ra8aGkrx~g@o{opi1Uh*2>$XBn@a0q{NwKL*R8vhjc z?oWmG_(`o)Wy|?fQmLugWjxr_asjcqydh)u|5{RkQemL>10) zSmbKO{xT}}6*NwBmHd9vrP(am2RS#kAJjd=EW^dCd18#N>9?RLneLj#pkH+R0!{`D zbj8_&BN)1 z?X1{zV0uZ2)B=SniuM(j_xlHrsS>LUS?TG<&5wSus9LX}kk;T~Zf@AaO8gP}O0VO^ z8(?PGC2Ieb1rCu@TFTB-_bx{yUnpDsuv6pmN_Wadi_b1GsLS4ZslUrMtJyxc{z~@km-=$EY>;FY z<$9p#<9quTOH(CS)fweGzpaM$De1**T+A}p+9b_v`+BGaDmVlWX2w& z89(YAB(rr+b8`-_ab-WRJmQlz)+%TYshqa zY11;xI~y=uWV{SFugyPBz+=ORAf;<_c+j;N@OyG`@j52IuB0&%G1Oq zcP>SlK4$0I7rysCz?F|MY*ZgE>d4i^-h^LPoor)4K>JQDFt@PKw_ILdO87W0^1#k` zZG#1o`*y_4`d39M2;~LaawX};>!?jDgGsewVHO?bqxh8;LmT*+W-|7?9Lo;vp@^VL z)F*WJ8ZF14{s;VUJ!Fj2PmWZRoY?H=K-ITJr8+W-YkCtVNh%I!8ppU~79sSFR<*KG zH}{^%ZdGddc);#MTiCH4#vms{J>f|kPG1RS#plLEfne$pY5vdvtEoue;d@MB8! zHlXrKwvx=B$A|7vI&<9?QwZmNDc`Ugd^mNS>{|`>ds> zm9d45oTw7ICCM7ziLeIRTS_iH>gR;HQ7tnnM&gRJ&pjx*5b9jp#VW22`A0&+X@p| z2KE}#R_UMb`%F&Dm?!f59~L%U=s0ir8Yw3=%*My}$y#tqdkNuJ@y{A2^0g}#iilKA z9BT3W%{EJ=OX$kQrHP<#{IfH2OhL{#yba7Tnt|y6F#X#Zt5DslfhIw)0wKH(jF%Y= zNQGXlP*XGbrIKZ;>D32pn7$Fy8e;hMDYM{@F%wnLyH>N2N{5~aESoq;LqB|~Z>;)_ za+OY%eh}oD9dTG;X@}grZjAwF;vFOz-a>5tRb9VqgTzjsI=1@cJY|}Py z_05dB&Ccwn(dRd|hofl5#$TrLQ{Lah#Ue5Q;IpP%G4e=n1?3OdlZ!W8ZEBe`;BQLy z-2F~hlZ}V3oD(|`yXwf)CGD0-HyoZ^`{Tl#4V==>`qog2_rlZG!XqrmIDiakynjET z1{$%Zt&U+F4A9NLzlBf}>_5Ut6?G!Zg36|%Bd&bVoNt(*$41u9G++Du)u8PA$}aR< zpOvxJ(t=Xp;VevIL9Tgw!P)6`TLDjlGfNS zI8&8n3k%jNQ5xT>JXz7hHAc}g&O8j#&K6V-!M|N0P5smjxD?fmNjB@sGk~^Lm%HGU zDnF~hz(AX*pR^->6mGBOD;7I5uv}M$1KA>-9WW?5xJuzq-jm&FA>gVZ%q7_h(9aqT zpwSqKZ=iN`gX36EIeEWSMrme-tFBOMWF`gy%l5a9sl52{9gMjX7H6tL4eBeQ&Kx%6 z6uO!R&JnXfjObCA8F@?V=i`Q*N^q2+Nn28Z&>UP2G*mf)k+cv6=*4rKb62rSy%P4c zSM!EqR;nEKU+(iMGIzYAD}1bH!LkvRacGZih8*j0p!$7(`DsV&OC8O$d{dyXLZ7|D zL8dGa8U`jo1*FglM#q}dJbMzfZ^y5fkWKuMzP7Hp_`{E>S}5IIsDJPAhyCo08fjh0 zZk=ik#x%Qco=C|`uv(y zEGce*1Wx;V$b72>z%KTr4nc&<1Xec7R!o`sUaN$}&j3iVg45)D_A=m2(iHBG%{Qw> z2ABfIOA2eeH6IMlzJB(z2i`f^lqX`2$pR^=R&5F=S2UH0u6`7W(zYx3%(` zpYlpZk2=GIB_B}; zzRXnfg1Fl7zpIlM%=CNneI|>TuAco1oXN_SPiRk$wUpL#>4=N3rO+oxxQzyuo|A1X z%hTUNyypu?JYi?zMa;>yFlg*e!0B?rxmS{F!+=mCiQYbS^@cv@l<M_^ljU8?_v9Wa=)CVyaoP1_&xCNOfeD|O)JZGW z)gU>v)*E5WAkg_5_*Tel*UquXIY=k`;6vAr3RwY_xjz zNIbHOJL2(wP1H?dEiQ8IZ5gQ;cp;|zZp5~V6rSZ$vY~hr3}HE1+uK%{D)h`PV^FG% zD-^qfO4&k%pD1dPeJoZ)0ekYFSNepDL7}wr?c$fk7IHN;wWAr_nv`>C7R1qPVa({U zVQGDTr?{2>FpvEg#C$J_w4r4pfhe~<-UR!D@iSl@cZp*dHdS432-?`z&J^{9g?sxV zYnG?fZ`LR6La%$5p5-YX5vyG6F9=QR>k(vhH467bo5`m|C`F1nWJlFed^DkQ`4FFv zpoU(hUg9}<^K><;3p5*;kbE4q5jDRiOTlO^Hffn2TwbnhPYk(SJXnL`wFCPp%pI->F=_YpMO^} zD0~lu{ALVsa@%%h)0@c7jhrVpSUcc<*0udZb@lny1JtCQBC!HRsGqAH*2#TvYd2)D z9^P3Dbd#97tTjz%{s6cn%_d&@+-CMgFz)m7@j{tN$0V7n;WPnC%P~8X>u1EJH!pO> z7HRW`@)gd6hwjiY#=cB##tw}jY4nW4y?s;U1B->B)d-6faMVj!iAu@? z^vD#Rz9A%uv#7Ro*+3~^*~PRewk<)JufM_0&ZGyYJnhrH^4PQa27HR+o~D&F_BbMq zLGdBCmn}my35GPg1#DOoCq$VnR63IU=kWaREi3>XRfw^&SQ?88)hdjAaF9d3bjxTh zo6x82;1)-%Ck@NAW-4?1YU;P!V#a2@%IdMr^3twpTD6Nx&F)}rlvpXGt^m6!3x2|m zHl_KLY|p*prHPC0Q|Zm`#TTvtfa_X92WOWD?C*_lqkfq8J0vpn-qk^REbyY(U-JsW z%n`9=J@)~wcL^|>cpr(41J87-4J9s@5X?enJqW%T%JgijfwKW9`P3B~*;Q)i*m7-^ z9=A2?D?BI|+kSqU(RwbNJZ&5`Ct4Vzdvmn*&lZhUEfMm{ridyMMZ-p$I6ZLq*`AO$ zvw0b;bTi=t$(Eu@ghQ?S>{wBf;)%r0{iW`8&34W0Kn`Kn8ZP=Uhws2`&_WLpD%)~P zc(Ehti6*JMwnl%hxS}i%hvCpGU;Xa6CZNhRjjLvVTc~M@Nd!dEFBRiz^Tg20dI z@UqHQM2v7xFY9FH*}Hx&aiCqY74)IjwkT;&`vMH&LSYb;nqw5c<$F_5Yh$#PXXSXy z3l$tNZxc@+NJ(Ks@Cc&tG{S~}it}_w`9`pE5(nclDTLz0NiNFeQ~#?h#ZumbJSB!d z4A`Ym=Hu3_*aj@cK8i@O4rLzRk+Vo{4weYaxv?6wSEFE;f~4D-f`<|0)qup8JDlP? zOy>?+|3;uIFkHI~Qb@BJl_L;GX(CIWnp)f9x6+vL>$2+$I!aRK#jDOr4Aj>hpiFRn zU45HqhM(6!GE|nq*d_i|OLN;;EpO}#>dj)Ij615Y26YN^>B#P-nuD^2q-XHbQjz_f zsYGtCKZ78Sz)PZ}a>$+J7VF6SXO!rk=0IO?SeZ^;w69}I0=Pty*XqR4+J{RgB`NlK z6P}aU#_wmkg@n^C%klcaFY_Ar`ua69ynDLd0KB`fce%2&ru%0^KDZ`?+DK1e3HpP+ zcmO!Gr<{7ac2o7%rgvqr4heT-zMg<%0dmTW33yj2NnK}4nYly+W3@*pS$`C{OEX!M z9U+EWxP!Wlx8>lLl*YL2@4R0iz{WQzinqzTv$Z6YsovcZSl%&LgYeXzDl zsHSsV^h-w+9RnUJUjrD#-+is?xm^s?L;4a59Kd0n9))ZgQeqS01Rc%flPDYL{uFSywOr=KcYEHG0hnX`3&O2tt$;xIVXo{! z$u@cW9SNU#q>9A@Df2Y7$!kcIO0(d`+pmw2Fph_^EW~T*<^Z;sSB)Qm<P|yl@004nHm0J5p&BOtj_>}FlklE!H1m0qhDQH8SXQf2bS#Or@KA`V*W7t`ftni4hG38H}<{G4HfzsfaR(q4KvF zhfvuxfQ+Qit95;fwspA^7A5xYj&f1kHl@e)trC966Qa{t^a7UzQomCaIx*^Ki3)pv zDBYqV3YC>3)xfwg7meRmMd@M#a2^9YL4QD@?4P-Ky8pI`$QswKTq( ziB1Jo1iSe#)LRy^GsOc-B=UbQKxWU9o|+}9zv^?djQ(_aWOUJfmUA6haR?QA_zBA# z5Kk5!gIk8jC6aB#Uk;$s8^^U2lRfu#OUCoI%(+v-`%|8WVUjqka^g4eyU56P>QF=2Uq6u9SypV&abm@ zvR3`fKM!``} zTI3MJ&3I!3Iq$yM{hAKH3(11R^?_$Vfhz^4J?bqs0P)+GY6F9=;EFlgK2PnFS-tud z#VGwGdy2FmX(V@&spozkN`T2z&@n%3M zn`p6i*pu<~=Z9~c8m8$b;FuCDEO>c}{PyY)Rj^h?X+_RMn+LracKX}0Fea3JG9TW{ zxW<`OTnNM6=k`3~84mnH8A$`!cXlLZ2_tl#g-lxOCa}wr%^V%+P9P=8V3t2yh~HT@ znNrsnR<0a5ubh+yEXfG8!>qZ;B9^4{N$A0EVrFL>qu4LuQP}0g;y%fSzu!`*9-nL; z8h$sf>#R%*QGZ+h&0SK(Fv(_~n_N9gv|_iC$czeWz{^)C^@>oB1Bw4aQ&(WGQ>sf5 z28oar%mKrX=N|no8RJJ^t{05`E%)_lxsPgFRbpw%ISJX(W{Iso*rFe{vha(h=HDkmJ2kW8coI&6#erg_pCZ_^ySKmc zYSxEwEq^bD5$d(i4uia<5Iaj41chKcexuyb8(;I260XQ@iXkCSP-v@|gR%Yhf>PN5=&4w%7_HkpRlcd+Cs!J!Im_Ede%a-XF+PmTZ9^dQ=YKw(3hILS!dS?xirbdw&BF-CPE zRV@q3JloWD69|v+Hf&=B8|+afue^QjFE)#r0TTenXh_*AipG>Cq_iu$Qp%%9u>w}| z1A*AuFB6nUS(l&HI-V@2jzwBQ&ll)=P<7-IklTc?15sU7gaA za>)U03o%%m2u%_SrCRA0UoHjydJcTd9KSTJ(o%-+HuT5QAB2Bch^=ty)pkViO}xu) zMPAggh;FKC+I3c{emqXdPSVAtVVYEzzOAjrI36pL>`T2?RDX2SR=*WbhbTiCv37(w z;C8~R*wI#1<^Rd7KkyQ-sng!sA4?~F)!cW?35q2y=09FWUa3wi*{aVL`8qIZoBKgh zPw%F=)6L^92MUJ0aOzCRrpqZjrk`d@P^vx~yTF6@ zr8-^*(Gm)oTM}tv*fyl`rfx1xzzYK#z0r>vmv`C++cO95?b_|#Of{;)oLE1$pcOMn z3FpWqne*$Etex#*zq)>Ok8rm=r1xaS%z)ZV&Nm&$9UTXP6}ee1_;0NJhTYeHwd{D(qx!34Pcud>`W z(EVCxsk4GptF|2ytE9BB+hxxEyGe3zTQBv$YHvuFf&R5z;`RM*HCl;QDbaCb{*HAkbMT2$VF{9FuH`Tb4(RAy8vw%F3J z16|0{gvvEcDhGJ0nWE>i$!O4mOYBevItiGmZTa5C9-6$E*a`VK@*&Aw-H=9pzGeIF z0}5v4;LoD|!0X@DeEfvwv^2kz<8zu4+(|YDio;@)y5B&rOR=@i-t~R-+b*N}fsgwh z;BRo>=JzrbgDo*)96~=rC%CM>aMO;VcN8A1wB4=uIZUpGH8xxF7m0%SDQWd@!XHQA zlqekMWUytBAdOCmkUz%D7x7hmJz}MVjkFP3S7frva3gMYL;F=x3!yUXzqA@Gv^pS4 z28F1#!a`m8Qj_f6j%x7)PFQ)PJ2~nigW196&lI+JY}Uq@iJsUEEiVN~=ulA@5q|GSQFmFNhSEV6iI7 zspaoZlw0%WULU&{9puU}N@rUqAP7mymxW^&lIcPOL;iwDm87bQsZylwHEBh3OjKt` zQ9l;;&xiV)7^F?QJI5e9K&DlGdvp2vEHJ7!+&`j7gUjm$SX@uDwStNJ;fGXBnD(HH zrSjA0$}hb&wV`)CZi+aoLIl;~tjXC-WruX6m>DE>(P!s8E~ubBR(&sv4I;%aHSBx# zwDxb#XIJ{ZKDMYHlK&&w!-AUtf%?>Nf}?p%Gt`)-_Jick+!tnRzR@9Ed}PTHK(ZouQ}AVTF7kXyHkYlu&rV2wR7RC8IK1cf`Y2m)wUu$4pm@(J?w94 za%Ndnu+L*eoHt2Iw(U@Mb&1`|u(E`oHjEN4!pmY^C5?4?&0#BI-5pjHGX%xA$_`{A zBsm)(j^znXw`4k0T%P+FfIKIcSQlehzZ=z)Vo59<3eBQK9)-tR5n`W(siJZAAX-^C zm0@y`G&N5g_sd*V%O-oX&baxX4)nI~a&elP_p?fa99+eqk7vLUCHHMT&$!(^$C94O zE%^cNInN(d!13vjpVMi92^#DPhrVqYX8F`q2V!+RqRdu{A=SKsz@@{4bT5HP51 zvv>633dAYw)JUkUn>Rei7EpErp0Eea+qh@^<@SGuaX0%O!@Uv^Eu0-!e|Rcip5zMZ z+!b#e+?^${JNoK<8uLvoWR=n{j-U2Gj*qHME5v`3WXnQpG9kZad zq*qAj2%7NXCqi_*EHv(DIQ^Pv#3~ zt8s+qH~VS7x^oNvT%d?f2oie-n+03H;Q`k(0!r~GuI+%?b-#qNoFvup+gTQ~!+M=w z{b($gG2V59ECdG;(#gwQ$nXl;wmuQdQxS`lZIIU9&r0J;Mo~|j>^R?KNF{b3`7ieq zJS|Tgp)2hXPjV$t+MS*qK4I<2vruDRXh28fgE9l;;8Dz0Hx)Wk!REcP$Q}(DSiKOI z18g5EXtfp7%=0yMqV*)*#f4gtJ!znkBoOq-_{jlr-RpyNW7#4zKlU+Tr3ApMM;MJ)DuOiRpS?bY2fg6nT5gRw*m31`KVkce|;OBR-Fo@ zN+8|6Ukj3BU-3#IIFH}egWX37j-A71$^O$uJ>9un`iMYQ;@C`}DL7%VNJv|Hh?Siu zW2@2JdG6}ej&rmACzH?aOnNxU4DE6w@WxlZi6{58i4-_&G!u_A+uA9NLOHgN#!(j{ zJ|VlMsnEW@&+-mLHk7esIZo;gUM{HWLWE^BtiVghVe{?K7tkmJURTOZwHmZI=5V81 zUyvZbMm*qi@h&*_j@739tg$Ws91zq5K}V9;-Ai_BP&mO0Wf~K?%v=t$jVf5Ew6l~~ zn{A!c1U$=trjEDW!6!nzDCIJ+q}NNJWB9ngy?Ps0zPp28PC$}bpptTN+LNPFPqSP- z#l$$-^6Sx1peIs+%bl3%?U$c?VG;0)Sx<&?J=L<^WXt8FgGf(-NN zd^PC~z9|rqU?L(^G}Kcx6dzaKKA(8uaja+Q!&6|SRKA`Bj66Qi_V5uYFp@1>TySrD zA;8J(hn88+IQqj(iROgj;%^zGs|LqvEP>v#(A$MF;P3C^Dat{iU#h+D$e z`4YOQU$mvPUHJbk`Jes)B7dv#M=}LM?+lAT9D)>;)=V=M>g7J0i?#^i@+o`9DSK(2 zxN`PsACfGEV&HiRD|mpDbgPIw8xf(l1q~oTS$Iz$XryZu=wCJlR&}y;@%wi?W9DY8 zY?j>mUjL@Ko}z(VaPmrMBW+8il8w%3VoW7kmq!Po^<>}*mm$CS&_!EPENyUmArSs2 zPh$IgSfnH4v;zt;$Ls*g2@m(5f&(sC?m0LRq;w7SbPbe$D~H8ZO@wYFUJBM|qL{IwdKlvWw-@3htkurTnj z&@hm&5Red1(CFka80_M(XcTITm}DHLF2PC3DH7_bg^iSK8iQDzVwz^IP0NxYMPKi! zLWj7_-5yk>O^TNe|Cy*3K@_r0&y?ew*L0Mbl-h9#HgpTUfZC}Yb>mL&)Aqy1!_GEt)$mld83R(gOwf&4;Qd@xb&M2=RNd5*$ZhMpb zSxfEkGxFiT@%V4#{r{KzztH`^$o&6nGl{L8WsKr*f>9I!)jPIv#B~%*nS)I?HJ7=k zLmqvEGzRi%b1f`A!n58s0b2_DPyO3k(jmMTq$d>pJguL9)p)azm(RbKTAdWw#Q*NT z&2j8fC)MCTtjTS^aPXIv>N}8%X-G<=Hd<{ZV9rTLg?uhwWzKQ}$U)JELr4JGp@|O1 z4#f+1!lF3R@BV`De&-Y^l=KT9JHX-_tsOwKKx+}OddQub`|h9?<9vdY$BGR{n_8!p z5ta)xGOp+D)O>Eb$?f3 z{#NO8n&f|!|5ESYx7q*bqRIR6@A3zx&;QZ~EYMX}hF-vW=3sN`Bg29)Pqo>;iR)7%3ZFL*;*6XbiaYkZXsYR#Aa zNn|p7J&0m1pKo=cCxHh#?P!BEr(sXCXjw*reqoLgl<#YqCiEF$lMb28GOHaKYLUEn zRKlhT3Iy#OSgc`AHtYsCog|WSmwtVeif|2?+fkDJrMQ-cOPNUa7sN8J&6LMZOJSE8 z#3>K7)mjD`BTK0Rs-x7P&EMjD{2$`{f09`z<1YwIZ${PsML)@Z>G!`ebog%!3jfal z{e!{w|8nmS!tCBrlAePurGeJSv(!`H1UneoN4j1mPi%1KiwlSh)z29DHol^o>xc%tNH>|6$Jl zAyVXjo7n$}dr0daqKZGW{|6o0KPK|OLb&{&M1MG~{7*Yl%*2iT+7S3j zh!xIR?W;FuNQ=Z{^WT?*k5hAqT>MYGA#ooEbFsXQkPm9ndTI`ehyFovZw*U=o0f}R zgd567_dHBK7YEgB-1D(;WpcXYNNjJZZp$p;A(!XFW5TR8^2h_1p|J-E6WwB)Ck2jo z#Y5e2%w*R`CYL1IbFHQ=BYo%gH20HEAYJI*Ul0|60}ur9_qA3+)?9pq^DK6*oaB1N zk&(EO@DX0|w5`%CeiV=Gd-BvvX`hy8z#c3KOMOdp0~*|RtXU;;YIpLpd@A15!i@-& zsjmw2Ko9BLRfJ=rVbvtO(8>Z6(#biWvPUm{zZ)LwjV~iZ0T_kJ&xr(rnw;(#&}5#L zy-AXLGQbiWapnClEbB-fHPMxVDZ%+$B}G?7n5z0TAzzxvdrQ{N`<@Q z5?Hi^XRnw<_a;69HqcB)vDxXpT*+5*3^bpzs?Hup#_ha5KJTb-Kq z%q1}WY0R*Tu+wLdJHZ$@(Y<*zoi_yA3W=b0Co=$dpvx3&>$&e|bD z*+rA}RA*Rrv*G=9^-!y92T4{;Dge7|gS>NdkP?o_ zPo4m}UsQa>UdMpWHto81<`0J>gHS1QKAldDx~k1e(-h7T9N}Hxx>ViUH0P;McFSCP z6i87}wBE7HhsRXich53PqbZb1#H?0=GIh%p8-}HeufJ?7rI7Z=qJ>1y*z$@~?c3r* zHK$E>$5RTrN{GgxUJLEyo)aCS$-GB}iFSk8e#U#Gz2rt0vWOibi<45c$X`eAxFUP9 zC7YPa?*I7hk4b|ANEUN_H$~{(!4#EVoZ$-oh!^SpO)~~Vd*F#M1!@R}M)HNF&e4C( zH3^wTnUF?S0h2wPCikZd=f=_iQ8F)wd~YaLUC*dLB&QUjUF6kGR_i){j3E+|;L9?!eys?9aEQSA zx=WeT#b~r@1DbB59qOWGQ5~T@k9KCVn4F0S@{kltsToa3&=b0G|C|Zyf=2(*4ypk5J^tWSQ=ecD;)jKkv`*5FPH7qgQ3KM44X~1mE-rM&5$}a z6=!3~iYZZ?@6@}46QDAAms`n_?`P7=$KpE`G<@52G6ijBh=2a2L3bSX!O*}TVt8rcr37BvUN6#qbR|8fZ1d zElxb*=ilbUp4G#n8Ea@=*bg*0irZ@?UOavD(MG`iQLO-5$4mjAxZ@cx$o!JJCid$3 zGXl-0bPG~rqnQ0Z^E9wF433KkX0?nnM^Nd&%8Fz+ADDu7Z%ED4d_BIY?3hw!xJ@R6 zrA8zf@g#zYchzWR!_#iQQ`-n_-gu3EEM5l_REvBZdXOP`yl2GXH7Ol2;ea;$70-!f zlk%gbo;ly=mB>(meaE3zfvLYBmdHn0ZD?MlPPZ7v&@AbKTEFhs`n4&zCFy+<5^OU^ zN+C=3bFHh^mkSx#h)nUIvhLJ}Htkwt#_h!Tik{V`K8icQUbq|T#o7YQ2vRE&`B9h< z0nQp(LSk;UJKfveYB~Fc`GX+1%Tx}HR<~T*US`m8tSx{()vUe5bT`k?yZY;+Gjrwi zAYOQBZS~np8Q|)LV{Ps8NKtz;!d19^y#7@4nFJdz(jU$l%&rB?a##eoiHh_*|9B*MueRD>*JUtfTC-26*- z1^}5*NIC}RWj~>$ps(lIliPJSu^(dDeSa)bEg_%0$%Qf~Qak|t3U@5Gxa&BwRQD$_ z^rtdwy?schBh5|}EEJkgV8UYjW>SzycEB-S8{**JB&Qnq2o7k4Qso?q4Nh~mQTXtu z42P-~VH;RnBLqVSpj-#qo`gS95)2sfHIi7;G=`nLeOk4$0qKzm?WO7unQ30leWK#c zfU+1@Y-_3t$Dk}&M2^B|#iC5H?0s4$JZCW<{tIHj%!7k6^CoGsOA2cu_`V2UyJ%#l z@AkyLX_Pv_jbQ1b@9dN2q)gCm!}uN*yu}@r!jP?uerskysUBG$C-Re2bor#r@h;rr zFCn?4T^h_g50jX`AmkI^sI1UDr8k$4i>JSrVVsR`+9@3h(RE7Nwn#=};5$hfP8qsN zpNi7E(xp!W`2*%ey+SxiCg5-Q&FB7D!`LDQZ7vmmf8V{ zK;#PG3b&$mja5zm8ocg47Q#tv@Qn;v!uhCruO~5hNrEJ&6CgPVMEX$S%}j3c<0EVd z6G`DgOI+y6KS|qdCK?W#j^&)9EDmb*7_eC6Bp_sb5_|nbnra0oeloFmF?{nq&u~tf z)Z>zZYmhGlg+6me^|Z7xdEE)RekRCDoex`>6pAgrrlZ5HN&^z=*vlhEec%lYTgYO~1;pYNkC zjqGIKA@#nSZ(B1R4_nD~QS;AekLtT?ax7R$Y4B=ulS*zez@Jcx=d{p*&xxF7wSKxX30d?a}r zutVy-o~-Pz@{lC~Q8124|A74O>f-yq&`n-^2Qy4h_@VNLU-kR6{P~^s@ARa9G=nt- zH@klZ&kAMnZ}0>WegKd7uj-#xlRpIZf9C3MRNTR+Fb0JE1(AENry}bM9^3bi*eQ!G zoTRpY@M0YWZIs}ZX`uGB#7DpKjM}_v+(cs&N(m1R&gXiEiFU)5aCu)x{U_^MU#8wz zcOID&VxKs^a9qhXR%R??|6;-V-)Ys-xUKQ#6q)foW8G*4g}0l|aE!AL{Oi4XExLXL zr;;&KUEiP^lb70P8>CKX&TnZE{?oj||7~7>TjJkf{S)VZz!HzKkM#${g4f89_Kkm; z=pTdrw+WM~-UD92cFU-g8XBC_!6=>S1HR<^Ho&}Y7o8o{V_N?VdWHoytGw-C7frv{ zAud|}1)&W~GJSZ9D4g-z0J~@w+$fwgx`SOcgWTjceq4LsD|-+4X;l1^(VtcA4Z9Dw z;vI1pY2`DlWBA#h%cW>YyTlLDLH$DCgoFP#br&g^nu+$qaCg%uj&68$HPMXk@*uLrn zB01FolH1=T(-Yee9OF+MT1htM0MjZ}T5T(z@A&nZNh$$X+BI5z>sQz%4*O^)j;+7u z0NW~6nl?!+eYi@?PZn%p;~O*{lf6{d$YrmKe!q?#Et5kJJRa!pjGUUTd~NIO0hj| zN#r5UQ!IdL!f~E+pXI#Bung_H6QJ9iIdDX0)k3{*_VzQ)#Vd^~{egkENy(i@0$O1* zk;Pu$@p9ldJpxAld65&C1r>BwEUMEiv?Aej=fg4fRMvEwfEG29SI&Kx*#HGpt(~HD zxDKAg>30x5Q{Kl{IvT6b)bQ7b4N#WSann+eJGs1uXurCqe4#I{S2C*X`lybdMaOMfcC`ysz4_GUoE97^~Ztx3?tWrt&& zZA~`WNwPMyK)@X@@zYU|+Lu>9ISs^B9D!o3E)%a*utWbr~Q!W0MHG-~PI81MMkS(ybYMyfI;t z9ckDlcUvGx*C6{VqXP?WoG6Fx44naGT1NbbnTsCB$7)Q_Vq$`RTvwc=WeM4?mFfF`+;gH*toJb#VNm|aV~%>?Tyof63r z*o%G?(qQ#gI)~kYR-$AbZbp12lxn+l11j%PSp?zd58ABH^inykT(ZY`kFr>I$u5uO z_@1<+tU?R6+?vw^ zty>Yut0qbbJBz4&)MfBw#e9w^S(N6aD0jtUde45`TD=LtVvfvutQAyL%fgr^4jKlb z&|p^)8pAe0M$}&`yXCr3YdUJuEs~7*`TpQ}wk<1aFuVw7E5lmW9F`gicg?OhL(O*J z(7k+<6J6;Y=pFkFSH&IDYw3WYwu;MhXDhJq!}9iw2zruW>vGG zJ1MbgvxQtHRk5bSnxMmWB$cu!h^auqH90HzL(Os~y|`lCSgs~RY=vIiHStQqON&s5 zmx(YA4HdukimghyE|VkhuO8l+Q9Q& zPI*W^gS@jO;ucG6;;e!u%hML`5+=X!758x9y(A#$kDA5qtr?#)V^@n)BR`#9GX40Y zs? zCPUrEklM(RAbx35jr2q(G3*%AWrP98N-fh9ei}bqy>g?A7REX`JRAyMDFMTIrwer$ zaCsSKdBdE`?P>!ih66Q7%0YVvqU`sxrTQCBup_kB4{ zM|`5bqHn-;6RjqdsNO(2s_+tez`1~FiY{#L1nH$1u~HIMMTn6JCltH=+F zpYK&%3Y`}!la}hDv!k6YhZeQQr^8J&m5ICM1Gz8BLwdWR3Q?J(6iH|K%nu3+{<@^p z)Yx>oV#eQG7s20kvQzhAthaizy=qcvr95daLw^yMAV|hA2)4?GPe~_+jS0A2X-x)_ ze%xw~f&P`zg=6ouV4In%hL(7tETOI%1Bse8QDV+NAJ`acCQ>b|`;88*XwT1(dQFjRBv44~3B`N_hxv?WjP7 zwzqQOsQGm+6I<$Vw^8)wJh%q87 z)4ir>;s7fpw%p5LH6&TvSOi3-lzNOz4LQSU7+NZZ@U=Voxu}~#Udy6$&FqTRd#kM5*FNgA&R(*WtNX7UuM~o4UgSZmk<`Pw*U7F_$j4zhLZ93h)w5H7C8-${ zVWUq`ro^s;KBq}BkY`g9E6T`L`rUu748$`!3JlfcIN+R=%#m>*yBbat&8|@Ln4x7> zbfa+eP7Zk*{wzIW-`yz56xr`<3UMGO7JrgVVWK2o;Z+z3K(yAmleYiTpj1ddhcWOC zc?}<1wVa|%MLkUXO5NS?S5?2xj=(})-)hi$@;u-~9g$n>3Vz>ICpVqafWxXyx0`EW zftQjl;g_3hg)VdFts;H>t%@IF9D7ei?Sl%ClD-(3Z%qzTc=D{#2t^oJ`@u)qEhp2K zw}6QV?Mh%|F~c@?OmxGpw-t_lyJJFWzE*}MLm9deAvcwLDPjk~u*^m=s2~2_xb=^+ zh^}VUJJt)V4uh2mzwvXJ>}21nP8`{|_JpGh?LB9}50J|@jhCvYz0qXXG_8gwu-mHbnffhi^-)nPEU%irdj5mwbjqBgeMN@VuMzI zGdm3{JI)A4scBN@n4t;k@X<6O=$e7|;Q%Z7Wm~Kj_|r5d#>%C1va&~~{a)y;a7$3Y zAqOJf$27&Nh#F{^&hDhCsA4H@fLyAtA#nLh*=53m7MEyJUwu-e8ep?WY-du;g<4TW zM91874sr??&YZpA)`Py|vG?cIj5AtP?rw0xq9aFBNs~x5CAsi_z*w~o#ZNNhj!cD1 zo2smBY7mkDhC$f>1u^4tt&BD!J~>;AYvew8i@0(G8J82v(s__Vo}1#MjVtAG1cM*Z zbU&9DC5Ne$xS#kt3hWyS{VV-IyJP+Wsc1aJ56g9oZ!dFj*LKC&(vM&h`Mk?HTj5oH z^=G&^dnZH0UYb|MZ5))9GZlm=wdslJAm}_%WJ%k~nzC2dNvvfUIU4adFn2X4$>IYB zPr3BRbaXB99BA;gIDn%0DtuB%ermye5{rXeqLMe;+(A6l|Zy|dEH!N*!b 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
YhX(QNHab{5c+@8Y#Kk5%WeUat!y02V42UZto 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 Z=+Yydgm`((LM{#bygNTxm zFk~bSnQwdUIp?|eKJQxJch~yf|8B2gVE68>-Ceu7y6RWeL}{og5)P5En1L$Da| zFN&jI1?7~iPz|Rm<2{fn@v2O4VO;ph@lN_Yz{ATu|Ivcnqa=BaL7VJS+a#AMU$2OO z6LYPKjU8d(4sKqpNP&HJE8glNVY-4h6;Y2-l%o%gN7*yNe-Z~u{Ssv?WKMLqw#gLI zM!+B4^88TgVd@$!WS71$JJx`CLoq4GEQVeASa_{uYFA+l?SkX?6*U)P8WeR$OyOw2 zZZ7tmjPs76e`VRtegcKu*5Ko@JxJHyiy}*RPA~ri#$!mj_{X?c!y&5o7w8)wMikNo zj2-#hzUoYn004|IJ2^QGWjVROW)IFe!#}ZA<+C(xroqcJ-SDnmpH0-lE^6)+v^q<5<1I5V`|>rd=~;e!TKU4ok4C=@MX6!oPy^rEqZab zxqDVj+E(X{6Z#9rtjk+BUIqlRTx;#NOc+BRy=M52)eQFH1+kVL$ik*ctj%fmIg-fM zI~@ZfN_gl|Kf@d}>xbK;q4O^=wg-3iME1huDVYM6KTZ7<8j?@I$$DUvdMI9{#D2Tc zVB12k+U=*^&ypgVS6o()-#(lB`BvZlt-tzJhtiyZ;i~AMCR0`9y`C32dCz%ST}bmM zpq&b7$G0S}Aj4cQRqp9i$I{*?k&XCy+J3#@S?^(PU;Ua~8EEEucg}+w<`HZ?>)x7R zQtyq4ogK`Uzf>t0^h14Yla9j&o5%@^a*|GU7K_s}XpvdFid>y=jGPc>;WD=60UpsT z=3W)QAi@7A+;iK=qNR zrL!Zaxs|g8g44^<1r$>N5SQ|DF}HL;xIrxtHg--Du-(R17}U;60;VsZ%B|`mhp@F% z@^M9I`>5$y`Z!pMSiz(u3B|ocK?05lH*=_$<5MT3sFwun54xh@-@mK5V9-CBxH(9` z3{*9sa?Y*@C_g7ZCpU+@mz@VMOp*{P?rLQ%ss&g0lLUAr0kd^;a}nj@^7QoN^yK4o zcD3Q+5fKsL;^yVz<>dfda3H;%+|0c=oREyaN&JHb9D%fSwR3T^b9RFMrfF{B?CvH3 zgMs&a_vezU;F z&e7$MULdporKOvl_5U#IU&i+P&L7kH=YfFT|EBvdt^b<)AC19Qs;Z)JXG{0r!&8P! zzv-=wTSQrwo@+z0`X6tA$g2nRpEkN}4{5040kxey<}AOgWJVl5={ z2PrE{Q3YpLM|1FX+BurrAh=wdZ2q|Mo8h7|8p;wdUQX`+snK|9?q&^kkbtS0TSC>f z|L2a5og+fq&HOhzd4z=cc!c-__yoBH`GvXvskcPo~%DE!U-JD%@oSmOaz2k@Jms))#fDh{>>-?@?<5`4b*A65TYQrd`T z|G4_cTz}4(>$leUYs%tW|BET&e>C_f zH3RnhM;)lXKyAqNw>tcjY2bkWZYzM2z+~f`1bJBO5mkOLG@hRf)9Wo$Kj zzhFYUqtJ8bmjS7|6DG3~` z2mxo4Czp-k_1#C;PE_l2@5ZBVd&px|9wnD1*RHER!ZVbxY37^#i{P)9?_)bcmNApK@_|q7l<49_+WdnB`=3c`_CLji zF(V?e*Q(GkX{Z(4L@MVNlp>ahC?p0FLtG?oZd4`pZj5lUB0uYM^2S!blH^4<6cJ95?K#gzb4ek-*Z>j^cSvdKiaP zgMTx?0p2uId*a!<+CO0%m=LMAxIgNH7Re}xFNAJzv|7;GG&exDh>JE8mg=8=y5ch3 zoH+O2HR zZ1wv^0Pw|di?|M{Eb}TvL1D^Ck&%&JyEjavrRf4Y)B8w z&&YD8x1*I}A&`kwgJm97ZR`7oYji+NjuPCpB*0Z#+DJe?D9$V_2)IeAr&@T;|89J4 z9}9U{;}B4(y1V?!cvu#oI(#aN*~Y5?v*4^@zXhRx!3g{5fk;t!F!_L;v8@7h#@+o1~J#C5gTpQ+|0u zB|{5KzIQ8$k<|iXEDPWE5K|@tJcTaC8eKPDXoR`ttlJ&44=m6k44GB^@$EFNcoZNz z9*tq<+Xp>=FBj2|1dwpXjr@g$&mvy*8K0>vV{r%>@G+4C_H}MgOGE@kKOs>=zBgy; zZO}V=dwUA`xxvG?Sqt$sx*@<G!vjM@ob2phdTv68RZ>1Sv1cNc$t+&IUGv%1epthf3_y$=P7wC> zod}>X`66C)QBhJF9r^elta8*DIr{lE>6H@`3%vZoD4N(q1UyfJ#@LXss}#a9 z;L7kVH1l7%C3Yy)*oB@t98!~iflq1@D_=g5n%ycc)jHzDRVS*#LjhlIj?r?3>tbNA zdOr=P6Lt44Pk9kp&0aoX4?e}q%d0<`V`*t=&|3R4b_2t#J1w@JZ^KOCOCAdaC8Z5m ztgVxilRtstIIWrl*S!Uwk(R-z5n9@jo=DUycV8bX>`&nu9vrMQ;y5ZSV<|})d?;*e zBe2Y}p0HH?aip|sZ*Xa;&WGyGorg)-kZ@&p8O3^cW2&#>xNYtoD1ELbpWfRr!hJgW z^EtKg&OZv9{yTP#--Rb-&NmSsD0l7x^yo9^c<-!fC*Mn?r<>&$7Z(>5oq8&mKDfGH zRkL1paPK&&k045(Y4|{|d}5nC#m1&_c4usC?5Aw`9RA#S9so<)d~oIPmqxF;`O&gF zzM37qJuSq?hm^Qk8DP$U35-6#1YU;bvBFxeTx5)%_kOG|q*#pcn=cy|LY_BbAY zAewwGi+@JScVg1)*#pUek^oulHvnlkX;Aep-Y=r>D&ZIk0Dz54VSxec-eOudur{QS z^*ZWVq%`$f{9zO5a`J2OfD&Zb;A;dy7@)D*TqY3t=HvxRBuVEn?BmwZnv^TCVUo5I<2#?P>HvC`m`q;(EO;N73}< zcx>Z~_DAHI`S_+uN(k5w_N8|V11`00K|bD~@gre;5ws~~ zqJXKuQY94#y>$-;NJ}q}v0%c2uwZS#0tQSP0D43#@)bz}4=P*sDxCc{J1NaF%nW`p zJmY+Yh{6PPk`YNv<(cdufUH9l3jo00F!h4sH%gTSmb9srj2n`OHkoaBe(QvoH{H31 z<5_(A>x95^Ud?w|xxHdtCIo!?s~G+Q;i(vX`Q4^cfmgyo|>*89+LFI1={ z6^yTc-8?dB^q>>>(wJ;AUb3irV%hYW>Zz7iVHEd4u3Us#mUwB`OVnvs`?{x^xo&i}RvfcY5x~*C2^d_d zEXrXnS_z-Stse&!dTb7;Of?&M5iVdPB9J3*g&GG3co;$^LkbgS4g#0}AXb8)JvV`e zEa*%*BectjJrH=u&_HU$>;h1I0joO*===voQS zriEW{+eh-{c#0$Zwii$Atb~>Fx}T-uSqXiSITp32+e=Iq+bg~<3Gk=<0oAQ@5&jJER;TcT2SpRJS4 zDsq&p*$nSr=KV)%C*c8A0HfjBmQscm6YISk_ABP>lQyo)%j=CQ#q76}X>)Qm7^Y|K zd-PXF2bE2Jw$Gkb)qFKIGLp^ZS3QqgahGUTeAq3$b^d@27;%mqOZec1g(woZbx#(9 zd*NK}W_zK4l&C0wi~mJSsPA3hVMe^&`4p$S-ph;VS>Itf5~;J3-RtuC^C8cc@okT0 zG>j|+>&1i*R-Zx(t~KYr1-S3NeEAZev!Bo{o;p$k-u$9juRd~l23NfUt15carJ`r_ z=z@LHwB{K~8qRT_@0at!+s8!2acZhNS(%w8Mg|&n&aat2RX@7#I+AckPeU`^=;@q4 zYFj^TG;YmJvtYE15nC@3nxydh_1LxtO|aSbEIN%nUI}UQx>O9aHHYXe0GS^>;0u za0ELWEVkYD=@pPt^H6cwd?L-7DyNi@@6q_Ue8OXQPE1Ow^}UyOT<2tEy`ZFIl`ndZ zzqD?8mJ}0so%jrama)9&^RSBrCgC2|!$8KzD4UEJYUu6lRpF!LG|M80B3AgzooZs3 zQ>p-Pp>1T?fG|_9qSWE2+2w?p@4}VmvzuRj?VDwvU7)T$J8;o$vr`Ogwb>Z17wly8 zQ{)$aN>2Y&_8=1AIih(P1XcQaoCrqWgJgAEJ>jHQvSe`fd(pOE9>CXDU(`=gav6JX zujB{r6*@QX_Ih5&T`y~&A3K&8f8GuJ8T$BxlZxNjk#hiJzo=nyX_^c%>XKakeY#4?+hFwj+K9Y0RJ-G1edC*-pmlczL(reSHd z*9QX%?V4G5^CpT}H+@)JJ!*cuov_bTV^-DX!2DoML$s8sj35a;QXk{rb`z~Ls`&8~ zwCmQAj2xT>U;lPbu;evMCOWAc>e4=-FoH;Bn@#maMnv?F;D(;^&Ci;$itLFsscAh>ERN3_rXb{$ zfk{8WR$7n_8o5OZ??jb+4n3VCG1oRv-NAQuQ<Im|nu;kwo;IFYcf` z13(@hN!4^~wT{)Ilmr5}xZ3;JetgjUOQ`ss6RCXq%^)Y5kQPJK#B>!~wK?3!@iU~5 zW#5Q{!?LvD#*k(*LOJcdODqDvg@w25Q$0{wXYD&{!Ud=DBff~juZq=V8X6jUg;=DG zdK&2qbZ`HgMz zrA6q#^FA3v$M?7i`d2>w`SXuOHt}ZDXoN50j!NQn(u(9YUm?7~RT<|%FJlI6@INl< z+$^hp@`>m5J)*m3_LgOVdeb@d)NfUuM>1f$zpFqumbvRmS&iV=aqAq#ee!eH(pedO zB;pIsgPjp@bc3VEPu$#Q;rU4FTrH|Zb}3aq_n zC$!E=O~qKglHC09=ZW8aCVn>QYJA3Bv61}U%g#2XMiceeqT5l)3fC z+XTo*dMV$F>+`^?3BGyn7J(G85)lPVrix>Z0_`IrC@J9jiYnY)m4(3z?h-XDa>j&@ znW8y4(k~+;4YZqHy2$uHk~g8j#1zJkzIRImGhsob>Sa?IeqT2RcUHOlyKl=g?n3?JN26? z!QF1k{X(#hG4&pB0*tarVZ7EyMiTw5xbG%548K)OZ1{WT;{0f?gJ8gcFshgvltVGx zWN-H!`rB$z6F5P>I}OF7wlhZZx%R980J=Q=)u4}{SN@tQ%4ck%pH>!{SIv%>*%KPS ze)O%ms)#a~`(xi-w)%BkSX>oPsL$u#m&}z&p*cz4aotiz13m(s z&m$tC_Nw?Y9Ps%y3xwf@#Y8t7+w8|CgOx=6e67}|$dbdnc{0mbo(*164$xWWw!OH& zI{U1zTHN>ese1wNE)gQHG~QUWafAy51F(uEy9q|$^h&-G8fl=1eaL&)KY3+DpOdXY zsnbq^(PyoA3KnBPg#6=W}d+xA9(f7)UA$Zb_p1Y-! z&oPwXk;VeGsomE`3u!{lkp-T_KnQ+f%?;Er0k>(n;Jd#C#GVzh^1R=hM{YibscHb+ z>CUtLHu$nnR}#DKlcBdfNiL^{ue&J&PguK2pY*&|%PG10`0RFxj~dTW02u`BPa76# zqTn)se~;k|$g+g^<%D2Jbf)g`W*Bz0(KOKG2LVse}ws_e_>}I5-~-ks*naf@$#ZNi&ugj zIT<%b^nN08GlxK2ul8rQMl>Y4ezt_7{eIrc25p{7bby`R`h>pO^-s!Hud{&$r*C`h zC~tHb{$@Mz$17fF*Y?8m*xbPPq|a<^OSqo6v@-uPDyQ%1rJ|$L?S3bC?!7d96DyM= zl4baSRJMFVt8yb=v?2d8-)Y34QxEX1$LVsB0?W%#3^6_ zI?Wozp!| zbope4pOKN#r)q!B!TI!|<>NR2SZou%TWYfmytiQT=fflqV@MughwG?p8YVw`#Z+G>poah;AtAxag})>PA<#ceMlZRU_mR8?(_s*j zEwNvzW*2xQk|+51O=q9Av#*z=d>8Q|avo&}%i_IVd5+ufqJ15R{e8G?xA7enRDSmb zEc%8r(<_({l)aF0oSB6M0y>2RK*k|0!vqB(Vc9J`Qe|wT6F#}A4{&(+jB$nVTM`D%Y1V10}U zz&1P@=gzHb8vM_XRnrCAgSIi+W@cti4^Whpl#R1aX+;|n`g{3WdV)_hWJ<|Y%U(Ma z!{8ZdX;W2}c#uLnqw)#RnG_-AyLo?omRKj*Z;)TOP$(X?HIwOC1rvoSVj~QBvGT?@ z(@l+<(LqTzIAo-n#p9aD!GoFef=b5w!WunJx9^wCJjCUQiv$etDrrpA@q-P4_n+E# z%nY0^1zYo_zfc)Y-=AG~{3{Hw#4$`9!-5~*pr7DdD{_{Y;I{=j2azTQZ+fo>>8~17 zvVA3T8TOUg->6g!ss{C&j8sL}kpwry4iL%0l?u!|7o1@eQI3M;b)9Rbu+Q>Ez_(@w@0bc8Tb7XcPE4XuVdt)AUBARm5A9JWj#V$H(XC(?|M` zk5)@1<0GvjJTo?2bp))mRK9zit~{f~*L1*oH?;az;d(k!k+ph81eIuE3I)6)6EQS} zi&q@9x-)2YG-86M33)=T9f(*}=>s5w0z%tgZRL z;qJhVmvKH=qQ6e1u4?p6#RnQv@Y}+R0WDH3s)gRNOB5;i zQb;PoD!i@CJmP82O+b1uahnX664)(e$y?2j1k8VdhOSCtXTy$X%ig8q%g2FxDOq=E zsJ-Wx)B~1sE3elSr8bsl7tW)2mvK^4Cwn;-%UGxZD2D5V%ZUPdZ3+ql=!IQ|Bc%3Q zV3hRSOdmgfJgKYn-(D}yzDe*5o9X1R_<||@uiMklkSKG@8pl=qUaEP z`WnWLdzh zU7Ec_VKsY9-mf|JM~}DR#nLsZk$-~1VAtAxTvp1%~1p>0Avt{gV zgYQH_bvsxU{q9jZf}jOzC{sD9gjr4Q$~~cUYF1yu8R1K{w7PblV>ePTX_f&%lYwRr zTm6J3huB=V$L_C^V$L*FniKSVkEs>-xM+Qi^7Hc9BgK=LyL4x%$+Q_e_qXQEg&{Lm`HBuJjEfxu(?EoL=0 zF4GeGHMy1VCbt{zUtU#j6ftb*Zs;S{yBKG`vCDmd{cOHI>6S7je}RpS4LRx4Z`ohr z3Op?i*eL#O*$Lz2=N~a=dHtlBKcT=hZT=A8HaE2?G{^6>6=gyk6=^Y$iNqw5=#HM6 zxT9F(9MO*x>6FV{zt2jhnZzXKu|=Agl|WL(5)IQ>owR2)_Xq@L9h(kq~&X1cnXD z4R2DUOn-l180CG|Tt>G80wR8Xv}Y?zIy_qale@Zu>hiB1*U`tO=WDs(3A~TnMrJ;v z)sM@4m)-m9{J9RrVqT9W`xw88fkDZnkksBB^YzaBWuu&&(ZbEhHg5p1es&YRHT%hC zcRX-Y%J+=qY5{$HmTF@;5M5m|8R1j>YoQsUXIBlvCHlXiIeQWMMvR5lBh)j_ z$iHA+G2(c}H-Ub>$-6)K)eQH?@TKCbvn?X`XeCIPCU8z7tHxsQd@8`pi7$;WQR0DV zP#@PBziAPB$v`jS|% zT1zo!*XIVq@80vR^ti?c6G>^godd78tSG#Mayp`roWn?9gy}<^*w438@$lBRyZXttpQC?q<5C%lumY9hWLBbT`RA3 zylzwfJ=?9ETUHO6JrRbO8rZHKtf-HCH<8D{8cEL_B;A)Q4Wllgb+nCsP-j2)5VppY>fd@ zbQj%AjRS-N@V~i^=X~jj)F?llL*EG4OrKx(I7sl{vJ5;;2|NW2X;x)FY79~zM}oY! zH6Li)uyTp~3a-ob6@C#*s?C2t^KNVThRh*KTd4C``*bujmv<=tP!CO?Gqs(j);8zN z)vq-X-hNC{@8Vl~i3NbF$*{R=YWxJ$Uxvx%tp{*0RtbcY)^1F8am<&i>&Dw%+Teo$ z_A;>t{nh!32|M4bEJ11c|u9lNL z(NRG9+*RM3FLcNCw$dRD2uJhs^VfnwNzglN3*so%$~#j{Vggkr;Bo_}VAHZeY;r?T zd4rYS?G4{MH`Apuwt|`Mol%;cB+H``yXPaga(sj?y>lW@%&BR+@z7(@5+37n18r4p zrh}qzof<1#ST~tJzLo+M{oVI&;M$^ZAAT7Iorup_zu8q^Lcn4E!!5tH+=zg)VyOnt z`J00}O}lf=>Jmq{%q};}|&W?6v6wkzz$#8zvXYj4S(ga1%yCG4QbnLx2!&^j5AE9(>_b_xOfLh?Te7 z+Wh%Zl0!M1XF~|?XQ>?LcSN&cgHt8WtEAPA)jg zSo(Py^R<`DBtxwAdmQp$@B6w!Y!)r)s{DErnFz8x`sO(L$}4aSf+}hn-CBR(xik3%^fu)&JTf z!tu$y^t-wkSYD=pLqoXy*Hs?ZJIywEtCUZi&7oV+x!ETyy6f!#OdOf`UR5nv&DFrzVp{iTjQj|?3Xans>@fR2 zI8KXZ4h;;%Mi$sjZF|(PH@Yl#fR%#Ib8odGM8|bY`VX4OPIW}fT#IG#!B|{(JRP{z zgGjk+cew#*{R&G;W}&<*^u~X>YPg5BoU>Q3B5x(mJf2tg7i-APMBb7ia9A_pQE*kd zIgv>Wl)d6Zev4~o9`HNfG6Y<1uC7*d`7PcUmO#&pX+6F{13NM3APM-jb%U$v*Bn~% z3Mqv~%E`TBUtI2eS!cx9Urj12_3md`ymsXG^$*QACML)raR0SU=j}LXP#cz(84dw= znKCcHt)PHD?twSN7@+X&OECC^?$njtDHim`Bsaf=5|-4mN9yQSuVm$fyoOc~#ZQKY z94<)ZzXF0}L)fb`*z+z2bhswBLA<}C>owCPD$SqU9XQ#`U*BZn>AAWTEt{npcMs!}Oev!yCLP7@h zC$&zp%N|hJ?%%%;rlgb~Q-Rq*4c0G#Ad1p)oie9-F>!Gs06pis3%V^qG1R*3B4Bbr zmV__AJG2W#N|F)wg`v$_PS76DK724ELT{N%%m-84Rb~n}7n}E545yqsdgqx5M&`GE@PXM!s@khs zXNh#W;Av8LJSgn!Fregki+06(=UXpj)79RmN!Fb*zMK*3xwH^y4*;+b6-cFE4>bCp zuiyJ65;0K4jsxjs04U@~>22!p#2Cn+7%s76GzS=wv;gA~yd_wGi~eb|?p@ltqw=F3 zECRb7-D@WXE?!vly>h+n47O-lu7-MF&%csd%xCz8?&swIP_hHik`{8F!UUKTvNIao ze%Q?))Uj9)F(|d)i@YHjbFX+v-Ff<85xwB=OQAq>6p|93CAHs-`qa&Jq88B?kLIlY zs1PR`x$SXB1x9p}8_HDYR6pA=ruJQhv#!?k)xF5_ke~AJQFpLPL-t*AER=uQ2{|y= zX(Tsr4-O2_N(ESq@A!gH7MNaodgjoLEz>9ov<*J*Y&ORh=sByr+VG`6X={9m4W?r> zFlYPi>j$i)z*LIIPi@;0S@^XUS&|B0SfU*?Rz zem-@Q=O`|xQ>oT5^@w zQtO7jisiZFJMo@!62h=TPmU~PE*t{L=ix`f*ATLsoy2E9 zzQ67G@a{cX?79A^uy751z_NuQv~NN$2*`0~)a9CNFuFK6-X-`egEuxE^_)-htFqg! z1YX3ZyR@+P{_@!CaCS?QFif_olst`&4rB5l&XYBhN%QYg0(;B(SGkud(SC4#aneAZhbYDoSN|)&a(39PRa<>>RM<%cJnHNl7NYFKJijNru8BFr(@4a6NUslq36eAn-w61)|Ed@_H%o zLJC9L>IBqvn=iRSa3Q041#ngLo(Ns1yB(mEq zno`3~bLY-ft=%nP;EzN(M`9enD74>K3XI7k`YCi(-NMu0WU?Bm@X1uBC?uHj)eK@Y zU`!cTo4gZ5VuYOM*kP3|XB#~|O8KM0ARyEd#ba$)G=EZ5em``!f$Ce|?XM!CmU|nL zEY0_})4cJX0K{K=Fh)d}L#C8X&tLJU3#c}f7aqM$7eQqp!@k;~z=6rd!ibiQ4c{ZN z$@bqqN@5~7qOxq9zyKiF#hC$q_b4d5O_Nu8dR$_)mcx-ULTkywm2AkR@~iNXgB-a^ zT6XCzo57?e)jR$3@^7`HEA$z8KKeA9lFKJ~!C3moKvPnI;P^VCV6_24YADb1_dem)n^$)f7KJ|0`*Pd&k;I0WR|* zQ_O=Y7Z=AkD|6TF)7>%C#-nnId(s|3EM!p>g(e7vr8v* zPwD8IT-wtg_0HqLCeu5!=@UtMcU+lRe)gOsi?rwFy4ik7P#5r&IBdg{ypaBYt!O5E zI%(#;8g4_9*~;Dc-4r|5NGpa61FB3GZ`>)F2RFP*)k)?#sbxoI3r9VB_H2EBOPm|} z5llrRg&fk{XZ#dEG)o5!%ZYz2bzUQNT^V>v*^CE3F-f7a*3gjZFsbVUDgAj1cIWaY zbHfL_wt6Y7WwglGQ#=Nf4(_Lwag%1H_u0Nxs!?CUGV|S~eq-+-_9iG;B!Vrk`AAJ| zXn5GO(PX#TXJFz+pgsbZYO=sS5))u`c55_Q6UrcASoRKB9iWEKG$4xwlg*5c!LZ=l zyK7}!5nK+48X7E5M&Xum6{u=SD0Nm9K_B3C zu*yR!Lk?5N7Tuml+Xr*$5ALR>6V_hZ30TCmr9o~C^vCo{8_oQvIcPn4+&ZytWtT?b zEdnCnwY3HJM4iuD{>m2o{L#>{2CyWA%86 zBPY+{tOmVualhk^`qtf{VW)Q!2%qs2Ikdp;EZv`c&z3lpw zDNpo1z|>#O-d}(XyF;H1Fu;pDP2I{miFcsG;s*oserL{)R2?w@Yf=m*Xb9CJ*VRuh zu}rnB8uVRfXwivijf9tXG^S_C`+&-^Gj|S$JHr(=BQj00CyL)gaVjd5+$l|KwgS`a^(g5fmF(Uet}`=lgmE_R2VyZ^;LbHQeEs9irO7 z0SIx`?=r>`_jteM~nv{&1ZC_@Uq|87Y8|y;9bMFC#sAy&gxMV<1o|L{Kp2l~%Mq z4kBFRQkR;joY)0A9g%y^wwL|((L^kL%f^*#4+9(KuIruX&JT;FF2!088h;~PX)Cj) zDZYPijE!&7d*D& zN{FH>Y?eB`v!Cxp?985h!8*!(jEv1>i_wqIN*D7Xw4Pd7!7(-3uPEf{Wspz6Q2CX1 zlVOXj%y~aSyzX~$;QaXp-}Tk}WoHy)@yo9F?m@ZP-?_6oB(2~P7waHfU zlfLQ;sk!X!g(Yf2x79N8c@vbAW=ea6-6=&Q7{Z1xdnvEZ15f6KJ$2uj)j#%IS2625 zs2-&nI=CSAn`jGV>ywtVSZmd%!#T?>bS|nKxm&hNp{I05_cg~6w7lNuz1rCk3rpzR z7p88P8$9&vBrw>lQwo?*4x*nkBO@C5qb$*|;e*l9(bQDxSu*0j5AVO#(}rHE)E+6N z71{fiA%UQ~KiK=fT354U0k0Jzl>}}>M#&4yOp?!Hj6Bn+S=?QH$1(bm>x2m=$v$BWf>r|Z@0XjN_6gN=}$8gQODiyYur<>geM z@2@Lg2LTIQ>|b{^qzfjy6poUN25a7LfX}{i@@>02k!5+0 zKHYy}4uLZ5n%lBmib*pD@u?0HD_Q3ES?la>7l4a2yCW3zr2IbsDl$ zPQAJKd?c97u$#V_8@-L{c$$9avmCv5!PWQ6ZJ!YNxwFx7%D@XB)EL6n=Ict@4aj%q ztbTU9AYeRL?pzsnaTc>6DR)(_5Gzs)fpU z&Jlx8_K-Re4ClWy477|bL7{UADs$-h<`kW$;m7fhIJFmrUUZDJO{! zmpj!CWH2e0Po$-%gNGk{CdM{QU#B-}X@9#I>%)>1E`(*Q;;L67Yr;r%hwr8`{p>VA_}b0>^4(ujoq*_d+G7ppJ3w~Z}n)L1pz+OPi_yC zlS1)GZjz8>OZuN(oF0G&Iza0N=6Z4R3=Sq0+UpO1a4??GN!qF=^+cM@c-py4Tm!2O zo<#okM6YpOQ>0R_mOq>-Tm|lieL4Ivce_{inICeXq+^gooPX_3AF@HJY0K`?^Z>z9w0-2;BT3*`EGp2v^sI zsVfInq@vF9+fTH~_Mb!!Un(`mIJh?o+*yAb8J!s`4WD%qQYM2eQYqH}y&|X>aXGl6 z79kWSCk>a4rIpBKq>ORhhG}$FDC84cE6c z=HKw0n!5Uig%K;$u(`GM>ZaNGs!;2xqa*hVCW3>(Ltx~)+|kb!Z!lJnfk+jd`nqoT zN(3J!Lwy5IP651~qAHoc^)2cZ|Bf(-(0~GIG6&}u_+9FPW4!B-)MtIF_0!F6>q(z1 z>LB6Pw~2UJHfA&ggM3OR%3EY*O-I+{>CSffQM+_sd@E7He8WRS@d*j^VjhnQArhu% zrW3r@PW*RjYHB71e45`Kba!`y=YH5o0`PNLqQMA^x|$j&&*g-m!wjBoil-In82J48 zGYGOMCoO~fJ}BNXJa>g%f+E2{O|ER6z1N&?1=Lfn&3)fDDNA(naKl(RXB>&9w} zOSZf?)@|&w?AxROgcO6+>-Yzy)b**Ae=H6Fx2q8%`0_2fyWKeovhreJ<+1!7O!+5P#;x z^zesL#I6|k<+7!mmjbEP@OCFWZpJvX3{c|er2Ny3h?ab z9Xa=H!g!W=V|*~G00&S_Xau!oth^?vW*QSfI#}wj&kK6V;J+OL{h%n!jSG%w3kh#a z?n?Q~r4g>T8(hfazDkU8P(j@}Y<icSu18cixEIfjZP!>~#s*Nu;MFCVHnb#|HR(p>DlescnGR( zyyM|6>25FNu6q0TZ$(@n^49ug(M9a_Z(0v!0hTSLDEJt&=Pj+=+4sD=`7SJwU@Y$6 z!q*BG@evHyk#nC|6U-hi9uA2gj$d^?+#L;Ye)S;Z)2`zsUJ8JaspIPU z+YIVK|IGz>9kJHB{I+OPK8%OJGeP;AjPsf3h!&3V%L1L z=W5q{L#~H$@wAF*h>BHjpcu-@$;r*_^w^5MR})rms7rrZJ`^jp*EGMy$`4YNMsW^_-t=A8_X6OtkXg))(JpsagVq#+hI#(wHimG@k< zPiqLl$VF!sd}fJlAgG%5y~%qKxYhGSqp%at8v8%9Ab}NJ zapuDF6v@j!(JnU1+aQw9+<(|jw%eq|H^aEhs?YXscWpyn99X!U1zmr~vGNpdkb2y8 zjp4JSL%?!Kw~Q(${+@WSA1)UpB`NH)b$oLAV?J_NKgwt=@cMQx#Mn4F=Z0hDuJ?{u zu8uZ}dEhCFfCRaFSOkdW_F=!nKGjLz`$;oOc6*6ADqGm>_b}=-;o}9{M5sIlu9_-* z(%dpW(nM_d=}<}Qeq-CP?$#gVN5&3+Z&clG)zf%dPJQZ{gxXPar|v#B0wUZ|R+<$V ztIq6Fc=I<&+oOATmW|@i9GduOQ>>k7JZ9~KwBmoM;fV;hybk9z5Tm$F) zz)VSAV;9X{$H3diml-e0)5c8aln$#uZbJ@wG%Ea@H`NkCzw7 zc2`wbM-O?6cUcvEFv^gfx7)t>+9aGhhY`NfLATyO@c0Ojt_+D2eh&u&B82Syaz|rE+^10G|wIuF)g8KGg)C_e%3o!toN))nW%k^ zGdDX6>2*wikEv7Lep-&yV0N;V$h7|mc(bH6rh#3MpP&E1U=_86Ly87|!lE7a@nTG} zI)3UHC}<5Ghw4~_fCSUv@$s?e*sy2x06~Qi&Yz|k2MQ%YX=x*JB_yolu+-NyiG`OM77g?Xc!moR9T3K~Jidt7GvhLmhJx$?vpf#2*e6aZQ#ccqN{o-JP%Rwuo>uDdR^EvFp8}wZ2W$}k z{o)J$Nf$G9=g@efet|7+MQIL!kZwof1v;4T!5J76WHjX5YrM~`LXah6%UDJ#-K}J+ z0aUqCvmD$~Ap?zjrh8F1Fzevcdz|{xM_J!E?B5`xXg(@c#Pb-d--e@PPuA{;pLwC~ z^i6{%Ddsx;&)@Nt$wSDw(dDs11!S^}eZHj;FL^%H@eOjlAz=pj{nf9Zo*&BdXLxqo zFkG^H`qb-`RuOSECVkW}TVf9b^2a(~kYm@6llyn)1*MDKJQjQTnr?-_5O7+tFf>j+ zJq`Ane77C2%so;*SO5j!Ds=%5}m{K#YlB9TUX|&dSO%G%(oN z+at(TA0H@+!1vWBClvt(&Cv|EnvIW?&RjnIDRF8G27UE!-Q5DZi16@m!1$$ed-(Qn>6;zZb9+5$ zdodijgLO{hxz7M~VH#FlT|MU@3NnDfq}(?k_4E6!1Auhy0=yR#bXQ)Qi{JVlhysL# zg%xrF6=CZehBxMz&?BYyoC?F}pb-EnAO*N^nwzN#jbFCi{`m1@ZEfxJ^z`;NutpFw zl&N3p;MDn^m^`1UuztS-AV)B78|NFyU>-g31yfUfQp&6?VCBOn+!`2U!MkgFT`+i$2w!zaB z6Dg%J`-51?+?pmqIfV_uE8HKSl(c%>zxQ;-=S>IAfFNAnX8ri}EBfG_vBjh2B0MR} z5n}}!5ts*plg7@lGG+(oI_ly$g{h`S;St3WL7KMpF zxZGv=p`aM=tVO-QOVL8&>-Tmv=XTjeS1<(C*^E}5SftHk4-SWl95*{8Nz@r1ahD8$HE?r(;0@EDB zbYLVvy#6dGCWLNgT?pyjMx>ZdnL@_$4cNj!LKEMFD^tsjWXs>;P&7az0zz<F-7D@~YA-=*m4;@@V`)5OgGclv1#iA%{m&KKS2>w+%?cA5?PYA&$)2;ctAhD=hc_C0u8Hp7t zg1|FW?An$>Y*|+H@aHtp8%aeF7#m{67UvXyI@27ot@HBpcKXba z6qQL9)%1)AdiK@IH-C5fn-5_Gs9&!?B-9xP?cD77EQvSdzNwGYD`a!=uSPanm#31h zamEI^unNd}u3j(DYxJV-`van%h$vC582!|?=|k;5V-UGp9M`=xZc{( z!CDL%Y@-+3=tRBPbR+{g-e;X=AT02dOw=B4oEb`Dcyv)h|!#=y_>PRbv7Zvc&{>%#e{ z*?iUS=;m?L`OJ0K)kU?f5;c2TAt%BcKlD94Qyz2+jTD=l9GByz=1b~YHMhJX`5b0} z7y%&=-rGH0LV3bi2(tlRAvGo`vpF`40((BD3_2?S?HNnN6Qi+Qa)_~7~0T7JVaIB-fm3Z z2e{_{_3z3>wT+E>Q>{L~Mw%~Rl7YD_RU7j8y()9KK)Cjk2J{F&Wifw;SUY;K*6tTndSn_~87mP( zpY-+@5JA5?=8CXzRQ|hZEv_k3$!B|4w#+iCrV?<5h}IU1k-}6`1;>uW7jy%dJ5IIb z;cv0?i!!(&Y_UXIFv`E4c{ht@w|!=+DXR7(bu1d2Zw+A}l05r$irgbiear#2_tU%> z4&Tl&L4KxB)FR+LuLwc0U%Bb|c)#2otOn+)(voFZQ#;JdrAAPKl*8O+Fj)$0a@mnR z61oqYUH5BMIh#soP0RNkt|4c%o@X3N5vfq_pbWFXd&jZ)w>KszGJCI)ZoLKXw?^dT zk&g%!%|ri=QhQr{TP9**R!ARnQb+%JRm2pr2uj&b{PmYZ3AD(MsbKFjA<$@R*Pjva zYE=ClQ@eT!q}Rx?=%>UsCKcY-prFObjN)$r4RTWlJ**_T91cnBJ3IPcH;K;H`^$tB zQwnq_g*bO2_}&BKv*8IR)qO>5y|!u9k2(Itw!! z=@VuHt-}5&b0;f7o{!Ow)Mq#DuYbuD@jr<(CxH(nrqwl=zL&gw<*Cje3~0@REe5f`8)fmLN^>2 z;`Y6Ayydbmq%Gv3z!lgyQA?g?bj5&)ydf?qVhE=G5!9-iX`A{j^Qvz6DJ%M$d?JMd z=$qUdo~q`Jipr|ii&-Paz~dgh%67n!w|bgRi#UVvKya^w04=NSK-#D4n4T34bIY(s z?7RxqQLUG}nG}SQyPLPwGwLo*^y0RrR@$w#RDalgB^NaR$)$bk*<9yViTD_!@BV#h zB5b=aTw8U`Fng!Wuvw%EHk>>~Sgg#y;=ou`n11?>QjL{6L)w~jn|W4H%2B=-*`W1E zUe$;aM|RR&eNw~#W|q)E=9t#l`9(>avZYMol>1^xz!!^M5rgE#uRyi#a_qmRn!=nU zGt?r4^6_pZ{%KC}Z$i>pK3gJJgHLe+a3a$pD57xIBbQqNp9{bbaci|_UrvR)u$|hZ zaTZ81s-^;!CGRi*?U*BV#n!;ptqdH+N3;JlCcoN#D`>ERd&SjsX^KG=F;PSWSzif3(v|e(WK1hgQ z^5bCQ;d*`5YV9!oux!kmjM%JR}_ z`IPbgTUUaGMO4y5NM7J&G6zQAyRz(y22v-eID+E^_GYteI4n~~h94&bd_|Rc6SHJ}t=p}FP z;Mu*}>Fm!TQjNeJQ#Hs9wKoTGL;N7h7Qe=b5a<%ZRdY*kBR?e2RoWqx+HGxF4veX- zGLjR~Kn3(~J%&D^QbTX3EerX!5BooRYOE{ z2;6o`>D@ZRY$8pD%K=@clH$ns@@EvsEK_rHK8G#RO)fzXz-P%go?y<=PCs4o8;4Cs zJ~nOUB8>$1KG!Zcx&0G|I;8VbALu;o>oPGjLmJ2Eu)Kg~@$yeYL+agpmd_?~>)&=> zY&chU-Tx^~y1$r$QTE;CwrC>+Jq&-}&BR(#(bBGT29+v`2VKU@{)s)^&m>dgi<64c z*Z+ug=tA?7`}6Cm43i^;5_#=tYAihjqv-h)>8y;#zNZ==4#+T` zKgXzSG08Tp8TiekWf7OXk(&BcDHINhyz5u^3c9A|+pvAoibmX1g(H>LoM_wXwH&R; z|6n7HYMb1k{q{@z6T?IJoNN4Uo#+T%<22Rs2}MtSk^%(GuLvihWPK-*SfomZpT3{z zLEN$hdHrRhW(!(gI0%wwwx=K`j6~04qdE@;(j?275GULyl z+hOQ^Em)MIo5`?3nYrV}>*s4fVvwPV@JGT%1M;j5j{_I$tcQR6Aoq$gM(9bqt#yx# z5IRxGfk;F+0m*CShJ>DRz@6$lkjcRb1+<2IQ9R3Vcp>MXI)5dt_T&1sWUnUPIKohd zRGbtsw0_)*X@b?sF+4mdxe{y-o~k$aZdNi{3!%5!&Q*mekFCaSU!0Qax6VgUTOaw@ z*5VvKCf)s(B$;IbwrvWcU_uTFY2~Lvr>S~4i?Ui~9MY_3XCt@{+Cd;L6LO_y5(mWI z3_jO1FEqn;pD%y_^!1l$ulr0`vtHO*VdjQ}pgYT#y7bZd7o~!%EH&3zheF%=k+`;o z$t;EC<@E4whTq#6>FIi3tl3y7*zrt)F49)M0eM=D7cX)p1dGz1I#nTCfoW9bi~*)g z=hXp_TpbunaYPQL*96@p5{$_)f~tT!9H`S7X)23xCAPP19s>q>j>bgNuer}`pWO~l zmuk?5$VR;W9j=2(4dPs)DR_(f*vwzgpOK2{XNPmUlUSlGGOvRq^ud04 z@lM1XA*x*KyF;}orT;|j*HELzRzG!}qZb~%WmUqskKR>0as5%mruQP3ln++e1r7+b z(Fj$7!!!G?g+@AHuPf8^xhZTzC@nwy56uoJy!7$KGz#-&l6=Dr(P$#f=j3@KBv#}M zB-$Dqxp_um*_V^dd2HEfjvSBY(R8MgJKE^uBwMVAH3rIZEXZB9OJhL-$b&3+YGWji zadLlo{Rm;%MwX>#(zDY$=YWAtXxrnL5E4!5=KVVL&U5hxT<4fRo@!q*LT5!j{__GH zGnz=RMY&bK0eT-3w;rrH(|`lX?R`<~_UQHXPE^-J$C2N7fJ@ii*uS9VuzDqIPsTM_ zwqsPElO`!CNtz=W4`~m6Lox+(L?qEko~BhVU9W``_q)po33&k^IP_4@7Z6CkXl?ZS znRYxlHx}Xdv<&0-+|8(-0mhK)2uMsKi_I3k5rWFOZ&7H*;f?*@u|=1T-y5i-PT$qz zHM!PVkHn=@k5N(a`=z#(%a&ImhriBa%tubus_;>iVH*Mr<0Pm|Y1 zSMaS|*VXl2b=%1`q@HtzL@P87$dX^hwS4E+D~Zy^YL9L>c0pI+yu;+@+R6MNa8spO z^2tU}KtMoBYH4mx1#^sYX3$`OWA4;);8lZO2ECS<+3JtR@0_tViHrb4qP~+{2d-Ks z3TARyl@%3{@!#l;nV+BkGOr1pxp7$Jc!Cps9Zn4+SDIbeuw+lLsh!FY3zds#jLY6Q z7616~O17o3ah5(C7|cF@{v7Zl#l^*$=-0|uMZ74HxA-Tb996Mkg{kMok*ZF0I>UckU+q5 z*LY^)^5?I?9eIQv8PgFKgdf2>&c_c(R%T{OV-aEx8Q3Gn3x(Cy{33#0)fb;r&l+I} z4EfAI*VosHjCqGv5bHE3z-MMA9D^1EKBzq;T5+m26FU6BjSGVB5Pn?X_NZd!Pux%K zB$K0|!vn^xL>;H6U6=bk_~i3N*~+5WP+R*qhVrURLjA%S z+I6PE>w2C&%^e^!2=beXwz-KE8uA1z^+4X6ouR- zm!cR$oBMOJJr(rOO@jB*i2a|wDiPiCGp*D=lTrB$Se6L*Wx4=i`GBkmZM}=R!ge$7 z>*o8a^QEoufNjqVeQx77qKy~BJqH9wZAYDpE^T~vCBUDWj{O;S@qZ=>|D~P!W^JnA z`==3s4^{$LBH^)-Sei^)4(f&dzl=dq`JUm8Yl6NcCNP(6)GpDd%!6T7Lvd2GZR|CDXsT3fj;G{BzZ|MluvhKLK z-h!-`Tzwm+TnS4BnH2bb%gyk?-mwKvX#JOqZeZ?kJ5gk5q`Y!oB5vY6Ls9VMBb(RV zg}#~RsP4Zq4}cCJ0SD)J;KEd>Hk8ZC#lvpKe2tDr3G(TQ7M0p3DC`yL020jLVst zn7A@kNmg&dS*ZS0FQ2aX5xP@8KoVObVO>p&Y1biy{%n-L zO|{P2_AAz=rlR5;uA?n{0f9!2*fI`&dY=HU69>@HCMIJr#E%`=!|~3JfmI5bO0KCP z%mt`>+IJY~R2_7ULF}I6TXg^N0(>EXqUkL7muBAFIN&bXiF^hBi;&2Q8YWEhIs#L^ zUw(j!6s^+#q|qg=aOc+uUWV;@sDS#!bGQs~TF`G6Ht~ol(+m@WzE0r&1tJPmma}tn z(;^hfKraZmvuzEr>CJl0M<~j6Jjb7j#})!HEDv{g!H67kC9Uc?AO#0}8~}hkTDPbT z`>Q_PFig`3`veX1rxX)Z^U~3rs**-$Q)3y^co~l(B8060VBi?lkOy;w_Nq`z0zA`% z)eJ|mnA{7R#qyO9f{6|hkrAr1bTD!_2iA_EYi(1Q^9DdpJBj%YE%vzFErl6kb1Nt+ zeI#d7BOP!DpqY)Qy!;7yQc%trTtZAt3Tdegp-Jf9n8FwSv3HjS z^)^C6ar?V%vVI6t(ZX)RbS}Mwg$d+U9h>yx6>K<%Q?j1_HmvzsEy7UProJwAvnePQU%Z`SOfRNYUL-&Kr&bFhoFQ(12no?m}@!hKV zGdLkPb5A?$Y&}~pj#-nQi9QntLXy`!e@z0f_OPdi@`BGlAke?~nD(L_Es6O}Dt(CK z7*-+P&sz!UhBTn+&c;?cF4E2Xl7?ox&bq-uX`Hd@;FpzRVt}hT@J2(N*oj`&!^!3C zb=fd1!G=eT8ED9GrJSfR(3v1H<$sHnDz|c2byz-=*Y@c{tKZml6Sq_skjXXZ z&+%Z4D!+Ez>_eN__`43_{r+!@0_mdy{`M#xTd?MMVH&k@!J*@eqpnr4OaKxllvYo zRO$V>B}(o#g0H*u`>l9az}^AscFgsDsBzi$V5!4$_dYtA^*O!_Myx|A4U^Xf5$2Un z%_TML3m*G^)r$(px8#Fe@|Bp>D*5=Pkh=yW&ZAfm`iGMgr;xHaCX4?vHv_`M+>akR zDQwfz97??bd)XLvM}SWdqGgy4T;S0cD3tN z&rL)55;aT>_&0bzsY<7xB%g?Ayc!#;uHt33w;TZiXdg=mJOR!I1Ac#yFvqrXy1vi; zQit_$I^eocHgsCrez`KD?m=ZLX#Shz9qG?DlCzf(!11uVw`ciVxd`aJ{s~k-&qvsa z-uHE6tRge?g?}v7JEC-UtvaOB_?Sk#uSv>DsZmy$MV2xSh z=G+Q>TWbI#)=&*7kpj)1pBj~)SDz{a|F*KvDX{z3$$aEq&nXfM0RO#v`rO&Jk34B zaN%XV>U;SxMB_&#awRwjw*bWmQXEjr@J%da%w>2m_9gFsE6_%&sR->x+bVQ?j`8gd z{4;G+B`Iuz_Af!17F46ZSP_+mLEzUUeJ=@q;CBoP#WluJ78rB40gwWcXycE9%=5P(*2nuG>Uf<5Y{Z z5E|ynWxx2!+r@)*7c?!w=j7}ohaBFY|KGljK`i01N+AhIl_OkR5Eldng`-wT(DG4B z2Cj5~#y8VfygvbMT8WRD&07jFp4?cs&&(^8;orVoYjUk;30za{tF`%|I*5)QdT%Hb z3zukw#^}YWI=w+c4FWB_J9z2eI&UvN^N`2C#=f4V^mscGk2`n^g_7gmD}h4c1z_1k z%kAcQCze8OaKKTv3fmjNn-KDV3VB$4__sBjPtm$GzX=SKNlX)To7ND2#u&5FVVjipc;*BZ}m5Vo4m8hdGT);@okFd@zMQVh_Rs|fJ=V(yLvm;rHB9gLNbu(QO62D z9igUyS-D=qx|3huRxw{hg&?{x$Wf@@_b5i5In6pTRm8{`zt{bX8n^qDxuYRrsP`y9 zNZ>#FvW}?e*Trxz>NC3@l6!SoC2~%5ho2J<3G{*_;_b%XlQBa`^Ck@`&iOc zP8Qh+D+5O*85Rcw8eGk~Wt3(w{GuVfH>T%FrE=x>kJpYE>KCwF{!m;nn2T+BZf-k! z_#;9hF(<a*fWuT#N-y%4`6 z6&00#=plM@EpmDwgbs}U*4HAo5XhcWPKn`z<+dgm0-Fd*f{b`~0SM^;Z2+)5>gnsZ zxi(DB%i$W62o9nLe}2|uTNkQG|1s@lUm*zdthLHNw85enO&@1$&y+BexbYd~#n$_YT zq5RI&41bQQ2i%=JDZwX&EY1iD{a9RPnxc-g1m&U`hYO;M0rxLD{gAbCy*>45UvFM2UZ4rnYRtkwxVtbY7YNg4{1G^uv~NKy zMv+FX$di|)z_9DXY|(Jti|HSdkK5ZbnVVOTW~CHhtX)`h(NDt9XNwphyaE9b-aA{h zN{ZeuoO++#s?N1w_L4Y|QWv6-pA=f5+^pZK{qFXLe*c;4yljcg`k-ZeB3nPp$NL)y zL6~%1ggK6dTy!qF-rWzij>4#^U}nHvYIOEfKZLxv*u2MU&WEx6aKo9^l9dJ`XvEcU z_&S+6^uF_&ASvqv^+D^rlBSXW+m#z=F$+4(rz|nljQ}^N zDv78|{JJ)hw(Ib8{i^Xzl~)57tr-@Gxxx*jh>Y$nFo}JxA5l;GG*B*FEbV^`?*WmU zhk#!(!~J7MK1dkDql^zbgfWkT16W=z8;>Y%l6JqS0#WVh;igq*pnz@|QT|@)sLr!` zXL`1RTjCnhpCGDYH|$$5QA&o%JYg&ppUcMy%KY*mb;80w^(7v=Eyvv4O zq`E6JZepsFdQyD0f>Cf$EQYO$m8<+Iwq{I73m4Y+Q3Or}CVB))WR$L{k+95p{Y3dU z%Q)7~MD5jG^rr;Ew}%WE9fxYfM2RLMqUd{j#Lw=7Om@kJ!;ucV9s_-U7GSJAOp)K| zi`?Hux+0x0@VaeLD?S-;DtcOz=}dQ@SC-I(azY#>EuKJjdE`G+r6 zi^8N~3cCx=k@-gejS#S_*aGL%ys?TJH<>H9c@5ue=8To62Er?OL4W4~2V@Z5L1?Si zI+V-tpw@EW@6oE9xVQ=gcw@QAb=7Y>2RQ2Zr^`dqCP_P3Hto$ENwNa(2OM4UNF){< zwGykke);Rx!o|76lz8j1cW25|fXER^Gt3iL&}oLyOVb7(D$ZCYA3L#65J=hZ6j}ug zF{^v!By-6>umqn_vm2g^j%FHEBy#V5!=&Y;AFWz7%eupSENPg3#vp?5S66^ zk|v<7p&`|px*)$Wkaj45Xj(^KbjTECcHrrbCOgxhJ?s+8Y}iuQuP>SQI&Hy-)K!`qoUOf13{xEE$HI z14mq~WfNGmyRp2R_+0c80|>Zxn@1*-ef9oou=0hzkV)Vj!1-2d_GTi%Qk0riv&wAe zCvXLpPx;7;oPhX4tYrCj$z-x;ewa*20xdyxAs{_U+gEh5Wyz`EBwnhi){czwQXsnlf=QZ@b+6jPy`>e-lUyYZqD&y>TI2T^3;i0x`pVgOp3#5t6!dB9uN z5|l!t=!-;-FKJ<0NfqH=o`0de0Kz8cezOZkt{u9LxX;nU=k&I5ex1vGJ;+NSkV9?H z^Y8oOMKLD-B?bVL!1$;MorC`T-J8YWQ5w-;T8=BNSju(l|yl&x+OO|N{1>G_evWa4~Qd zhTy7DLr0|Yz(&XOUe4L=9G@}dT<85)f^jUn46V%d+JW5reITZ$cL;l+f?IPKk?YRpTqfRa#q*W?bG6W)9e1`sh=lokU_` z&TbxGi#c|ceikDZwNdDmNXS;6SP5G5NP5(hu2x(UNhCjMN7>?CVSzSD`6RZ(qot3G zWA15-os-irlex=`~w<&r%mVv$uZk$fd@_hp& zJYkL!{d(iw2qhw7V&QuJAtqZbD9%O%yriTgR_Ha<-tr5@o-q<&|87Vembha^jCqAb zeefEOnw1*&v>XABi2;~J?>3GXZ6a3;r${{ka`g%u}hRQ?5gUD6!Q>pM|OB*HA{N0tChW%tFp= z{?}v~V}hi~e^-q5z=A%xz>#fy!%Eu9T*bKZ6HE{-(qdhTzWfBzs=2+ePJBjugg4-H z&|WZxw0r*Knv0eKFfGS34y}43;vq|8@)Fyi4#6mD^2SnVacaY62zS9!Jg!=bK>bDI zKimDDLd@5tlB#5KVO9Z_sCfVFW?^T17NLM*a&XP6%oX>~@BHhH*Hj>o40_8{I-kO4 zHkR@77)s}?&lHkQXx3^hDGLi{U{6pShHr+kgx-z)oV)OqeUNuVr?}0fM0LvCEAh-Ea~Ti7^J>S zEpv$9H?`eHJ@_9zVH-vkG1{E7B>w^i;0NSQ?gf?U~;m#J@#R2MUCNn;<1L{ z(>X-02otE(HX24jv?@Hlt8ieioDjIS?EV(-#ml4#KI8{teg6Iy9*!UT$CrrFvH2XF zy(z#Qwk(_3(^1*{A)kvb$RPPI6l1tEKd~0VZEdXIN5<6XRT}%;A@z;juXgO@>UH5e3<{1D71 zqmmX=K%fvbn=ahv&Nd{KXpk!336rYqs4|?8I)xh6SQqZa5e%c}TT(dIXW#fs;i`T= zo*+89y_$O0t0~lr`$Xa;Zs_CEXc@iv3OMNrh+Sk>I1bwIb}eEEQ+HZy5_33Kh?7SX zPdFy@5t{tH2r@%o^?ocaOdIM5%xvifShUM^6e}QuO2jV>3*OUHt~gCf#*eSaH-kkn&J7NUyNk=1 zqzV*^9TR+kLREI6$-C%R$YT_$u)KF;lSY>R9rHgz6Q9$$$5+g_Rhwv7P2l8&Xg<+#al2&iDVCpK?^A|m=1p=N_Y)Na>@D5Hs z7U-LwU>hyi;!7Y)qHd!Cl?KaeLg!N=dtq_*bd;J>c%)@D*u^@B#%s}`K?3-W(ar{s z``iIX$|8VuD5XjaAuq*k_qMRu`&p)4a;`1mI)yJ$Nih@^XCM9Iq9<}>$KX9W9t0w* zDa-k(>4TJ|0)#n#qh+(qbM_sa@~bABCNqT$NdPks`x31kLSca)jD~nc@3+(Y11ES( zX^~y)7=bCztb*D7>#VNDwC1xGqPp5O$eJO|Jv3q{UADY`)~J@QPb+=v2`-r$Nir@; zDz3kRIhDo-2v9z+AU?FAHJs&>1As{hwCe6OQ~kZY;Exs0u{I-b#;*JF0?w+OS@U}c zQV9{1y)>lLBi>e6@_jGg^_08@@usR-!{(f2Lis0)*d?tau2`vT!Np=plg2Bt?t64c$hJ}ySRFsZGxaV z3B`vGUZ==&^|s7k>D9Zv6u}hE)dsfNJm5l9Qi5 zRf|7@LOtnNwsNo#>j0(Cw#%?E^m_(Cm`C=dHMsh^neSezDUr}R2T>}4eYLf+MUCFU z-5uiRB?Ujz^YZc) zv$6s#>s7_tz9b(v#F6)!Pad%)2$n1=s((%poJ|Lo3BKV#0DuXkTL3w8)*o>JtSbLb z{7An7kRn4vQ~s>PY(qQd2iYCcMV#C)YAxu-@Dc)n&>)=ML*DPUh;AFN%7>(|?IjfJ z8IpiO3IjV%!Eb_HuB~dGLm+4#ouhfmG{BIV4b4X?Btg)&f|R|6bs{;p*PRfwas#wY zz@e2)jA|(mNFso67ZxTPGZ&YV6?oC(T>+2`9FV}}|nk;&UywZHGb4Bi$=hsR?1t-6~B4#cw{m_;eANmeN;^b&=)W$d5HSIT{aV3uzyP_{`lt|@x!w1150#ktoG5DCpMg+HTFTIx-gE%-Mt9( z>XyUn)!X7ZN1P*c=8x_AdGGj zu4Br#`wSh&Upr7i+i&Vx^8cc*NJ7pV0V1l@9CWspcjk1rdbx?&r`I@%X zpV4)@C4JZ|-GCn@d-ruXMxKb)9bn6npbE_0#a&xZV2E;F6LR~1sAGJoR>~vh zU(4vBv*g?}u;)cuD?pYL-dtXY5hlL#*!I=S@oJa(Xa3(Et9-9(j~r9d`tjMda*vQrd&Y-H;(;22Y9)we6`K_gR| zfQ_fB!K~+dEoRcxIu>9i-_wO%Q__$Zdg*h2W`xE!dQ%P6CqCOhv?^J|u&$9tSTK!) zC_CWKbs$i(ovJrjGDSxzSxVsOLAzZyAZ8wp?p%G=_(>83qQC-oEQ>@3xl%IQcfiRT9TkP% z0%QUf78YX2IMOpR!gKWBM`1QfV0vQ~XxD_&($S67?f;9t@emuU8+ZWlX`n^`$X8wh zyHWre*t9xYkgicBSoH~h&TKL^IFfx(D7J7Xkq4p!Ba3jXVB(VBYSF;msv-dsUabA~ zPkpZ~6?-4@cSU9>aZl>#?yqcV#o@&Aj&c#Lux;`NQ%c>4LMoNeN6xG|E274zZO&7V z4P>@eNAE-XW1g2Xy|tv)eC&zB?FQ)-?2XuqG%S&+5C=9c=x2#{Z1TxW^6|?FxzW-( zIi7!mL$U)ZX_-{X)-dHSjC_`7OqyIYP(R8mI>zs1UlyjkbofA`I9hVypQCCi%k@{w ziAWvXWSnLhE^bJZWI$4O7DYxydi28VxsXrT_a`y*bjD3CzCmp>^YsC*c#gOwaHYno z9mN1Jad&})HqT3ID)cRR5-Jiwz#m=wtAJ{ zlZXCePUzhn$IPmOjyAP15nP7ittSUZm(p1qv;0AapH{DO5yVKT2D-AcITL&f(U(kE z#r*qS|M3Fg6rn!~9O;NbM^9w%b5Ft)#$XNm5i|Q}0shd3lWP^uwFf;AAC>oVMvv*Y6>Mxui#=Taiapi}kO@ z>!bveT&V7gQSEyrX3bSC4B6drp$c5)%S0il1?R^J3TTR&#NC+@AnpC7T6G!TQX2ZV zc_#+GA4vx^U8#)nwuj=`m>D6M0BHONkeK>q%zyzuB# zX$y&r7$s2I?nNGJ6UlFT($6rW^I&dIX{lsg&q{i?W}|hS9sc;~Ys?zEJXV`!6B+}6 zn(+Tc)ms2n6?X042c$z%x?8$iKm;VD8zcm2q`Q&u(2X?G-6C=5R5*aNNGaXj`Q5(% zdEe*x_Bf8CI0O6az3vs)`dw@k@*fcpwdPp*hgjc*Mf6w#wk$Oc0kV2ph&Y{1BY_wWSnVrieVd5Xyt+89v+SpKatOh?a)bt zf~C~B*jRw2{KxtmypSQS`!!+46}+jbUo?1~9?iq^18?5A-B@*!@DEGyv%r-tx*P_u z9j=zehYJyYW?3q?se}` zcw_KOK?VD<@|SoFUcLN3D=RdnYM!Y7E`2@nHBB_K#5>jE*m}@UVWFnSeSSZM;m6K7 z6JEa^E}niSnaVGzzFZj*gZ{FQypX&r4bVMqdpuseeC{;9UemsH!Y0$^^|?p^?J4eN zYu&DkgyWP3RH*O`E26dl9F9i2C*~%PX5}bm=e77pYM?9(DqJJ`t}V@=igt8!zxp!Z z{_17}@)cHZH`xBOc7jthC-HZDQ=7voilXgv@?m)n-jz@vt2TT>rmizqTg7)WLGF$A zT3r}&j^Fs=>z}-h!jS%X#k#*N`$f@zkco-+xZgg#iZvy-I-BZq?ZxDknMxV~ozi=A zJV(E{&z%K=8_~^@HKqS|2{(A>sYZ`c2r=0);6)>bi=JNn-Iy5WQsk{KBdLJ&**W$6aUB!g+{mmM4u^yjlMG|9NKbIfBVr*Ugt9!V zMsU*0z>X6?Bf+RJ^BbR+>qp0!%T4TyKmO3PsB@r{tk8MV_!)S(P(P-acuVsAkDI*r z5;DJ!Zn8EaWX0>R#@sT=X^f*EeW=FQmz5T{(P1nft+D1Wde_S%Q&Lu{VFza4mU8uu zGPFs$r9CCuR)6G3nF_Rax?%oTj zqPB}AX#kEdBfs|5(QGU#Cr^60g*jZ)oSZE(|12I_H=A}LOlvmxGLfOK2jc&d;cQy+ zt0SSCiv=eS5k*r2A!C$x#ss5B^o7L5?US(VWsxML zKC-o{Wg8ld%A$UnnKRH)ZCNxpTU0r$WkJw0Ha>6@1zAL+0k(g?cW;IMn4UeJ5HAzH zibJ3(@d^%E+yz)K1Y3c;!onoT#b7uAsOJ|4i}U*)2z>?w`W5&8<{_I1#Y&MzZly&7NWOA!3JWyn_2KgF*U8 zabx=g5gopB)kUJ~J8SD^;~G!VDr=(b&DihuOX9{ACEhvAVADZR{aCSAir<_VBPi&t zSWWcDrk3(K_mr8v7-1m!Vje`rxS|&>8m)I^1oC++I@X zb^SR4=abyp@IVtH-YkmUH-%Wt!+N=aAyUowr=(x!8=_64@w%AOtwvFHXWx2aL$zsF z@*%g#g5TvYQFD{=irz?X&crEqPn|6Ex;ZC^eOIJvPXpw(M?gN^%7 zJ!J1z+iqf)kAIG?r>DRneN1HtPMkPh168|Amw!D??tbgeV=d-CfeMuIM7a8xK0ee0 z-p3x$eGK7oQfG^12}KN-;zmab_L1lcT0vcY{F@L#I$RcPK|)8bMt6c%0(%?cDxpvx z(W}{c=^J?TonA7AK>Hn55ozc3h~q;a^TR&2kg;l^`d$Z1B+18U@8m|#1=>*d;nu|i zID7$kms2N3Wxif*}&FI8v%E`sb~SXEDh?HCBL!T<=c!| zV^UnvyzIi0QxK@>sqeyqlJ)PUs)kgk~L$4}U7y zvP2dcL=JO#Ps1F23{l=m|NJ>RpZt6($usqx6v9z?d7piyLKQN^vN!ol^2JV^y%8oG z!Uh)A9ND-)Dnqd<*8L!Okr{mPWga7%kA$TqAIL4<)fPMF$f&`h--2h^5wIix{9WqJ zBkuU0Kc?+^Kd<{InE72VAO)-%8i z>GirOUW6crRkpiM(|mSrZX78d1pb1c-m)Rx@|`TF(n;FT<63ihCI4V_>Ln?f>^Q(BEqb=L6XTap(Qx!^()zhb3Vh$M@xkI#Qg? zf7*mP*TdyF7&V}u!WmvB>Pcy}Aw;GW9s2Exv3&>}5nDQ~=zJJYs5=jS^%u6(IPe;^ zy7jK!uBIh?(%_}#JBM$LQ=}yGC#}1VNaIok8<8^u;{%_TPcJY>$v!1XDbR4@Vz}g_ zePk3q&^^ln{DzaDUtnhtP@<(}X_3~?=68mu^lmDw?FQH$OuB()>mHsAT zk36TP;Y)5IS7u5BYHyjw%Zt)4h85JG75pbOXl0#beXYAwCkdmckuc|LmV|KDq6M3N zugKGSd)bxqMC>roes#lc0;z>Ksa}EoRr1w>l2E{z(8Ch(;|ZHD^|GYo+(j5)w^PsQ zX%ygEC{3I>HgC5k92-P?hJ6VqAC3&G|kkYOJm`rYp~-K{O%HdKx~ zH09AM;7?D#43oi-RwuN{AT$5xG$#}NsqJvA*zv z!ev;fGtu_4%e4#xPO}60D~iK^uFyF;;RgV*Ub;7Bq8t6~wfX4CdWU_E_Gv=~-& zErLh)N{zb#6#=jVEJLcuh2mmjK2R8`%JhKU;c#fq(rq%&zW*T9LzV>z-z?kFZeS@nw>V^R!rw7aXg8v;zRTr6NgHk^db&fJY9*NgZl#VK@6G$mz;6luSGvpIj5Xq@OEYXf-(9|G?7UQ(Z9ny|X}M6&F6#Gb7ikW7|J>1kd-Cz#U#OkZ zmc{N$oa+ttOt};8*$dxTk1LwUutvSrOuD{8hOgzv?%_Y@2OBAMy+5SjBFg6}VwI{* zB`}6tcb>A!KA?ks>T1fFSa#Fynp-P-gx4`J*;6ZolO#JxU7%>ql+% z|6I#ar#H1#NMQ6P#-sf z<=lPlJiYC_iUJl|`;P8mObU)of{s-8G0Ruq!Qggg%>N+@m@L^g!7vC=(Rt?sa0Ly5 z6I~t5%yz&g(Sz9XwIJR*!L8TTmC_1C*oFVNi1L05)67MH^Dl5 zz#-1Vh_s2Q=xjRjExao;`3VC$nnyT ztIUs+(~dra+Kzs`M^`uedFk8tmrL%KmmS_E!x17C}n`%n0DpD{r<*MEIm$*4pO(B=BOA8n-k}^ifGDBvpD=#zVY;wQ)3PM?Qc%RAQ#dw&ZiN!XWWIjSqbFH^TSgjFQ zUahIa%cpRoY*Lz+eyOPvn!jlRI@dL5xgc3gclS%kQWQ@yODiD$0HDIq(ogFtIwek0 z1`ZG<`9y$BR#x4f=vkTJ$OL0ZX*5=88PGcb+KW@yh9Wy3+&(Of({!ROw&%Odm5o5r zmYJCe`W8!HZ8Y^|5{tM0M_*|TOeX*kB_o5?Fyk_yHynF2$Coq62=798bzkhZp908*m6YG|@M?N}2}Qku)1*c-x=dYh`5+UoDjOmV<|<-;$R z3n|x9;|88yBQMDpVnQMUjI@NZLdsaUMwu~Cp+5ao%C~7^;sTA{yQv8Y?gJV}U=>O1 zh4Xt03yYS?vROX5SFD8{EWh{Fyl}f@)oYa#`@RtoU%2a}5^VybmaJ*AIu9$4jqiI? zYry`hQBWcA3q*5ie{ZQB_Cn&D_oP><#kpm{^nnVMFt?akrrrz(IVSOM*-<8*)T!IB zFl1e~*Fu$m=ukCnJM(4HfjTEE&BR!*)4|EVO6Plex_UqWg=-O^)Qs2W(Kt0_)P!@2 z=Bn@E&{NT!r{T$QH#0vT2kmpR5*x({c;$8WJ}&07l19$4$t=U zMBVK604JwKC(V-sc;3C(LHpsTuLshr0kXuq2kJ$jJUcruY2Kcm7mCH=f3@U)mUr8E z(%C64CKfoZzZnRlE@dvewo^hhSVBpHJo^-eVq(08eXCnmTBDvGyl``R><08hPtkn?dU7iA z*0Yr;6tdK3MNb>h(!PuR>Pz&&-LNX5zTGC+Z`*BGP{zPk$RIM-)53ViYYJNSjifnURi;k^)xW>eD|L@E2{w6lWM1#3|uXl#6BX~clL?XJ538d^Nlru zz2W1Q(G#G`HsC<^ss5c0d1vNw;k(?aB-JHPdk@H-R)>=-Meb5XG1F0nV4+nR=hMw`=g^u0J5e!>R4y;Ha$?#E)Gf1(UhWd=`jCF*Xkc9~aSMf0D#RX~1{{;r0|te0LbfmuM+ z3{Z5-y?#AgVK_Od&@QhlKsEYF$Br>4vaHq~JOgbCDb;e&~_I%29VjFKXP} zk9@G~c?juZLg?x)K{ET`X}BL!^#pbGZu~8oBCiminB)sg{1*l5*m18#O4qg`+!i>M z%*Uh*m~wAF6kLwgS-r4%6U$U5$i(x5cG?*l5VLkk3a@ zFzTl8RYRx2-=9@v3)Xw?;08g=edr(R;+rQ=oB{%ZHYNNdi@BPIb{w$DYUkVrH{hC= z9#n}qE4Gs~zhpoBo%_kBpSgWk*BjPcfAS!Cog{g^O#n-;szWKSV>Kuo`u~+)tyxZt zky7b%xQ2HA;G_u;ow%~M2t(*|j=~5s{Z$%S&hp$fQT@Zg_n}LN&Q(A}5_y~$Ct*YE z3;P&@9i$+lP+KM<-r;}c?qZ`Zyorb>P(M{TX6DFAqtc!Xl6o)YQwP_jw^f0*R+G!aVZH%a9to1#`b;5 zv<%2SWG2)VW=|l(iZcQ2xs(Yc3+UYMrn7W%L^gj5NRF9nIJds4$UFV@s{hAp^fnvG z;mFVFn7-^StV z>*1PYYV_FF_xJE`j|G^@GnQ&@jWFT4)l+*D{;nv^8_m=_ao1_AcQMYV0kXx__cQ8E zs5+Ff9X%N65Lia*7KhBcp$fjPB>swo<_GCk9_a)jL}l_^OEZfg!20m@XR|-(18GY^n(GhWCks-y zU*$QK(+=dMRif>C?{o#;)&zQ(j`2c5g>Qb=c0pd6X=B=>ROd+~W5rCNeB#rjFN9p8 zUh}F+v}igy`QUzdYALjI1!NP0kEYiC4U78UX6ow@S!Jg4byUsLks(GY{7_qN+yoa> zv4$8+9Ih+0&NroCli?3AfV}=SC~;Bc=r3fcW>y(Tsvxava^JdqdmnfXcBEV~3^GXs zwFnpa8|fs%@uC*?=T6U>I{kHz+CRPPZ~S#X0PKCj zHJf2pz(4NT`U)wvEirU!MaE-Pz^BAB4&N^;E%myl%@-d7K9FD&@#<z4=0FZm7y)zJpT339`34E=V<^$_$`V=4 zW+xnFDN3M!L|g{fW85NaD>M1wg4Ku+!g5>$9*Y78M>z2qjh<8co=sa539pm9#e=p3c-yuHad zc1yR-^t3@Qm!goQd|%>$_0`gj<3@Sq|8__Ft7LLBvA5U8h6LOPM#C6*!nc3sMHoJe3AHXFN`Z-A zbCJyrMdn$4{N{cd({m0UlJ`MU$$}Z@K1SMN*|FsVLEB30FF-=bC0Q2&48eTbz|!Uc zumuGR#DKy9NHxbkGBQBqJ&wxABnka4Qc}L?;0eZ@U|5m{0PuQx(1;|xDh(pa0pUNt z85H581D(Kdt%Z@R_kqkDKS?e^9CH6gmF4#Iq+r1vG;fA$6`paiB8u|`GPT1fD6-ij zdfwVSyttWMWm1V%(#E>v!@$GSOR2AXI9s}(p(8S=kg}Mbe6iz*$D$HP-T#~-=CAwt zGpl$AIvQ2?tDf=Y{Io=bTCft&)4m=5zghsG`Sd;N{$ze3lp69`G37t`?z3kl?96vE zBWfsRq>Hk0*nH9*=PsOhhaY91VIp zd_;9FeY#`3Xvjxa>7TgCgjn&?PojD7r72Jbd2-Z(>;O@Bx2x+*ISQTyqB<_RF(e#U zp=O04d@Su{xjIpcK_S?cqvS)x%2jJb{Zm zrWVVNHFt{M6m*DZtq3=~m_Li;%gxF5-FCm6t_j%RZcmz_K~h`(_MW4>eS7^i53xq< zi^u}kSnsf){CJP~s>(G?%>C=@nT5Tfg}swKc+XkFby*n*HFIFq64b@NPp8&4B`&Au z9d5s|dUJUCW&e98b3xJQLZ0yi`=iJeIo?Px*p+V3tmS$vl1b(TIcr1cIu~pLwW;~>jj@E{O zObY4P^a(Cipfw;F9~VXRCY7Vho0mlbPCKiyfW@$Rb`}A;jR9QQt=p&W zjo&1pvGAZ^Fq%1yAGmQO{AEyRU9us}+WR2??4vk`S6QM{;T@pw>}cqX(_f@_E2U0v zZW2pA90&fJ$8>jFI_0C`)9kFmFrFhOKYJNAw2iN(!X;@WhRkrBZ93FBdyyoAv~FCM zTQ}Ompa2>eM=m}#n>zoA;CFDB9);xZQ&Y+1iTyuYi+TEb&>$=3cj_HwHU{Q`@V%_0 zKWK6(zeoi2b2b@#ybh;yJB}K&A=oXDoVW`Bo2 zYUgo`BXV%#x76uuOo+(l&N=(oDS<$$I@Rf=WK$Y(7u%)l`lW54;tPeXYhbg zwy%S=U(mM!tX|N12f%eLi0f#I&8&5a7AI*CRL_n)MlPL8pDq0y-$KoYED;jKpd=eA z1LgPP3Kb6p89~UcSjRlVF9!5Fjv|5End4_AykiM3pG*486c?TC4X}^|g0+gy4z)?$v5%MfjD_CF-+tw!Q+|d|pGTTHBYZG@Ok% zN@bj8^lFf9r3Rkb`ufD;x@x0J0A$tN`8k)Pe{W`W%ULAm zn20B=E*Wf7!@JK|raVG_a7ppwUgu*? zuEe+bD}$ju%+JL((fs-<(r7xjIKgN=6O|?BJ}BX1_X$7BNGNwRZ*o*N}~1xoA>Ch=Meo*Lgqj-S}YOKbpsuJxEWLAWorFdcB0ELc4|8El-?DpKi-SZK#NnkMG<{PfXp{B%?B@>JcvR8Ho`e{9E%uAouc;UDUUQPGAg#PIeKjV* zeKtiGTNta_26r0eA%&Fp^|3<6+b_kh)(CtZnvb!CmhYZgBB6(4yQ<#Z{I&~xaIf)Q z{IGmA`cIcc*blll62}pNhgq5F>B04pp&dzn{9bX=qFnCWJQ9wV?6xx4w58lKKhS7Q zYa<2(wNcg?=|aJ_bjc*(;M!yWQ?1717o!A?DQ25AbmR4O8}lloa{2bU7Azv)f}6Ya zq@-l@Q}dRezl#w~0uA#>Ma%nW+J|I|n?i;m5;>c0puYJhLV_eR{PRQt-wkL^&spVC zVk(}9$DM@80J=)%$i_enIWaLYK!1b!W5vu|k%_37+cnjq?lC<#_jP9iC|!C3)Gv0e zJF(>SV9*shRpqf%h`8 z2G0I69nOPo))@TL*2L|s!~$hqpHBY{#N03|BG~b=?PWyVQ;Xgtf@I2Aw?6{(XMamFBMf*(8b3x1p`g%Qa!M`KGId>uVBTn33X&1Z~(HW8%BBtfwu$L8dE zodNd%xC2|%nFbK|W>$RL5s7VIl|8f^{a<+Eu%EsN<8fq%4 zizQIU0OMuZRRZ%jUr)fE%BToLy{R_P$g4jQ%PW+E8pf+cRmZBebOooE=1wkvB6& zEE{od9f9-hmyg!O#El1v4yArx=A}GlH*v0s77A1tnd(%c+&(ich66&_Xdt7!R904v z^bGf*71=07@{a_Vb)JEf=-G_9FtqUR`04Cb)4R%t)+veyM^>YCH}EHg7<$y@07o%ljuD!n5E zq4KE~HW|QshC>10?t{3Y`HPq79#unX0YrL+b!+Xp85uhsFdk)>gO<(Mn3xoId%(2< zUFM@+9LY8?vH_HoEpA7!Q(#~RE&L(JbKGQd3c%@FYf9Vh?-y8?tAfNo;{)Ovq0500}%&t;JQ1j{v~ zS}0`9`MkJ2jtlhr)0MZqJx6`_2er{av2b?=8~kYSQI)_$Ur8)lV2C2*$5PKuN<~^e z+pt>=tDdtru*>dUdU0#K?O)>+A<})@n^X1!y-mf*)#t@@1IOQ%rmFu0iu{$Sl~B~_ z0pShcRq{%V$}e#9Ucc(%cW&p(H^;@i;qvlO7M7ZFnho4g-v|O^NMjKSa!`&|lM7cI zqLi}J#B9-F|Bf~q89O8N2Z^@!yLaa4B8-V$q?!h9h>&9bD~Ak&W9OJqUR;+7C=&;o-}$R4pp+Z{NpMx9>0*t4!5`4s@M1-U~`4kci>>EReSFdXX!o(q8dPfX6ctLvd5mxy4{d zzz}rBtuqg_*#7JC3KRazphzn3vjUcjy<9e*eZF6);CgYmV`uC+*mRI2&P~ew@$~03 z35)y}teDi(d$I&$)*?sm@^;Z@W+-~vvaclPrQe_!Pdk~A=7wt1sJ%R}PxUzM`AsO} zz4T*3rm$(JOR+u8H;yK@Do^G`q+Lz>P0z3~V<>a()pimrknAIKI271Pp>^||Zm&d~ zu%qD-BD9u&V_5Ok$u%=%>o>Wp;aYGVYX`{-myyqQ7;P6X^h+eJDF=|I^eh7(NdI-V zrT{>`8g?x~gHwE@Un$)?J2}LEiGcAM-CO$!i3zJTntC^mRL&-_&yfT;5E%x89ANYa zK6zno$(=FoQ_#!+nXUfMq~R|V4G2XDs&A)29$cpdULO^V{Hi+{=BK^vQ-0 zB2NTt^yl4bf7ylamBCL$Bu}_-M5D{Gc;|p%uxWNy}d#hXomCM`o zNaF0BrLzBix367#ERZbTxVoGiQF{6Xwlr#y^Bnrk&ZE4}2qTnb@j=j((>j*Z`m?kC z3XNOsiIL$f*Zm10vd)K6^ziibDrCm@O4`(1-BG7IPd+B1C9yI_AS2ez*y=A2w_2^R z7D|^hVCa~cbI8LM7c76=2CTO~dG$RTb}!bX{Dxv@^An=;Sj1l}TqH$J%x2uTWr;}K z3F?3QW}+2r8-q(Oscvw95=`>`F!w{FC{Kv=3$~xe+C#p}x0oR1pD{SZJR5TBVY49V zz2}ZUL)3W2MSHAkOr8h)ouCp+$9`C}eG3ZW7LoZ-*HpaC3C8tMmWpgMxk7vUax&ef zwze#W2%Z5F*R}5nV6W2r8^eaKaorQ$<9b|IOP&b)1}552081U%uEQGza}5rTJjV$t za=law{Z!zI$HQ~nN1(d*yKxsNG{+HGX4DZ(EzLm)i>!#!Z`E}QWK%IDd2LiHtbk8< zdNz6cxE1%Xbv&4>vu~y5ces*iK?*VV@mO~5LnP4Z`YOt(Adf_+7RIB70KtL9$_JeMmu2*}gKTtd|Hu zXz%5H=|`q(1>RI|AxhgK)YdJr;f3UXuSX?8Dh;E9HlW*{?@BML?RolhAb>6rX?5M>7j2Md-JN+#zz13Mc5mJrC|g#*)_pSOdy;V4kxgPDRbFE3z{ z2=en2envM1SXIID`lWS?)QDa8qbX|AFO>{ehN2IVn)oqIjI}cx-l##B0X{xAAn{xS zb%UVe@88x~P%r9M!Ix;kKgt;t61}OlWWUx55)JbTw7TK%Y!hHcG@eRbh%%&-drBbEQ|7Ci4lXI7U+MKI(Q+bo848(XZ z6rV!1w7-nM7pIA@*KPdbq0WFFfm;?|*WomCW>0>D&muj9f#6+R(Asjc$#uJq)BcKM zgLnbt9QRuVVj;vqXf(aFXLNS7TQ-UE-kdWi^$|KBTH7yY0B}G?yB*VhtL9+`5VZmT z6m`*UT3Z(zi(l$j-D-rR|LMh#AM}uX0c1RVyJuXvKo-jq0>KdGV^d!vtv`>})e2?Y zP*L9?JMW^0q~{YUa`{Q`Mx*nC`}==DA{9x^XVzTpZZ5k z;8G-&O*eS>_KQK%veQkBZJ&jyCd&L>F-4}s$bm*f!5Vlx3WidA1Z)rC?AWbDCNdl|3Xb9)fm4LYHbF$?}pJqLqOBi-mPJW_OTz!C)<7m zz3J}C|D3~Kd09^QMNW(0o_N5FDvA~t0ypQEVJj!Ii$~!?LW3iyj}IeNe(whTLi=O3lMwUPmj=)I%@U zcti1%q`SgT7p5!KH)uu;T&lnHt#6<7>l&Xd#kHARv;C%Qxg7NLTb+4)LR%^ut!-y& z8GIr5SHH&szOcOpvXgNV8ofjRj0=>WOYH_@HQZ|?uTdUz*YgPjp(mPIwW za)pTT*_n?1511wocf=hOo@dQ=5$gS)JFwPS_@5RqvUIW1$Ru5_I{baEg4LhuFwJ5Z z1#M&+JMAy>wp1#VAcX6zU0v`wLjwNUY`DSRX<+x0FpT`4Nt1J({sZ}1RRWsBUvJHLv#ir-Kgno!r6BwshY@gAya z?-|Ik1;ST<C$$?db6RWw=#n_^bD$B4W5Oz)2$ZwGa26z%4X9fwaH-o2Sv z#$T14`$dDn8x#?6Vp@vN<9^yxoX+A?fQp831?j4#g^a!?@J8th&*!0-XOJNfRl+Ef z{=vm5!xYO#M<+GGj&}@zNQ#jKF3y7t2F-rDh+hCdgr2?t{_Q*EO?DcO+&QwRaLu0o zxKi5%?<~nJCaMZ zhRgO;lL>@ zgcR*Q5j#}g<9~VG85``=W7kf6Zc;)qcx$6VKJ;DbE6#JnD4w^3iwp{umLSKDE5&e) zMx%IShAuJ4s_WUIf(r$up8XVXEoQ6pr+P&DGtByjWk@YJcke)aWsEPj`5&`hAbhN& zm3{Yhm9xaw@mUGtXp4q)ZKZJxg{Ye~2pmk^4z_PhjMr?YUBS12+d-Tpj zFcVN!fM4H5P`?;g;MMVz$$O2~=vL+=))`PrEtkZET(K*EkY2h?Pv2H4*$;W5MmhFh zA0Yvwgb$E)6R70Nt09tF9=0iK;Jx5=U;CNYThn=y(H-1=-z zh|jnUo0gV+B16RaQrMr}^NdhyCRG}Q=QPa3rK)difAw((vm!BIFj{QTaCVh67XJLm z>ofqe^hlih-&~y=kx_}s#l^9A^Qw0+5Mu`oR@>=|r!%6B=TQD$CHTbb@(v(=wU{K@ zZ1;S4v^pH|`5;m+*7<0_#(TMB)%wI+pCRhKbG+7c{##pY4$>WmQj4ze@tAM#$S;zx zzngR)MOj=aNGbiS7_ZMI517`x?YCB)=4>qIC^MSYz-A}yd1H)*Um>jV-TR1 z?tMXeDDwIJq7qMlINAZs^JxNJ0$#dZ^dWG!1=LsssD4^MYH#0-?qlp-i}?-aeRle& zWrL7IN5@;LW^&AQ+1N94XG@eulPgIC>-mf8&D_IQP6<{0nYhJ^6fPQmkqd;pO*9Jy zXx7H*bzHz%0T@h#-9KD^DqpQS`H8;h>U9z(h#Ulke*gE6pbKl$a5~r^7J()O_N28& z4HRj)5#aTCm`GCaKKKDn;E&S4IQa6P^v|wLA7PCjUWEz;`nk3QN_8E*7c(U{;Ex>t z$#MtiZ!NrcTU`p^3-3zL`pPB(noHL%T- z%Vo;6SW^^>05OU6^J;er%K_eWVPP`dNIQKi+cV(i!1vJqyuzgY;;NAi7PWj8W1^Yg zwA|71_5c&TPWa7;9yAS2ORKj(1Z6hKYmCB}i+Q#EHWdQZ(;lG+q3=wBybqVgPOj?p z`Ih~j^YJ+FChoD~@kTOmWAj4mJlk{WU+T&ncw_abDDZjWiSQ26D^2fkSO^SOhTwJb zgE?JaMkbBLc;_jlS~*Fb3u{uSL;XVBY~XZ&pkr!9$gIg@0u0|5sgmR{VdSq21kP_p z+8^(Gvt44&A8y!X+06X~qp`Eue&BtSIvzFMsP^(@O~;Mxto!755xdg94)-=~D$WB5 z*?de{Q@>8*nq{xmnCe|EfdP*#^-;1&2fWKTK)q7fAhac+Vc=%^$4euIk~u6Y_Y-si^JopT1W=QI8;OUj>S>T=Wd`beJ`rJ*zNTb7 zjDUJ;ltR{ulGXBsD_Pb5_r|9TJvsWLNa%u3WTqqV&Wxq_=>C7T0Qnj>-#!=zuAYNi zwcRi#etEU3S}1aVQghcc+xc*g2y@-cfu)IIR#fzck>UlINB=<1@0G0O!`q@`gu;2h z%V!Z}O(mpYG!{<@&99Z&o7e=dHU*q_-XA_>cHZIyysP`rh zAAby|#qksZYM`|C!&%1;@6(I@xRtQw#k7)zle*RL#JKRjL*+>~qtC7#&Z~0Yo4wyh zR%!LJ`M*o=>mHN~gC=WqT(%#!KR(=o_0ZN)QU>Qx(Ir-#&)?yM<^uQvMaeFs;G^5Q zJ?u154$lGh_TV&?(zmTU)i%yPX^$- z=Q~wRfp-9a*wxE9h!72MRDG(eqAE+9Z)D~Tf#Z2=<`Y}d>f7@}cqdPkb_Nk;1Fx-i z`-e;kPcSpQ+bX~rwBpFxiu!*q$-j4{1qY{EhV;Y~@gOG=3bk!(NS<8=djzv4Fdz5B z-{!N#zENztmjSD@f!ATgogV8uInHvtuX7uRJ2+?9EMDon@BfP`tHY(}eRbZ!=>A4A ziz7XM*S$!u33I8H*Y{?QImd5fpyqz2=7IQeJ@AD3-m0^~o#O`<^@7nuXW&Cg;2oHY zIr{G!9*F)zPvkirzhwQ*@@ZIN1`EF8;MD4Q$cZ;%+0bt`3TKyob8NQv^b;PG1c}Vg z-d2CO%uPmtVxVNIN>}oJ+YkWWMC&S2aYwG=&90J<%M5;h)1E$JZFb#^P2b<&S61wP z-sl4D_K>x-GNZ@_zcWW*)5f5mTBRMRY;qUPjEr)E_%GFE;jS<@UTPGA6D9u|1HYw|cB$^h|MXq|39 zz8L|}M4SJv!}4Vj$r<<0ZiY2(Nc^x7nA^RJ_-?A)YX}n)^Nk`l2EflTN&i(PG5%Md z)hiz_nmX@m9?x^ZZ+E$!m!mMtnakZ|jq!j*cSA!%%gkWnE9-VAe)is=a>0?x_nBjx zKrV6iZ^agTQLT}3P)ibwml;pPy$e!p!4a2o@Lp%($BWMM)y%_=%L`EZ6Gh36ri=89 zQ}~{4j=cQ!ttvf5!?HpDeewNo;FJusqO!4(%>_cTn<-rEnhxH4nA4Y_hjszRu@EV* zRGGxJjzfCF356Z63ULgXH!VC7Un^&KfhxM=;@++QN(lU7Ph&tCx$V=gg1wm~TW`qmGZho%i#d zr%eG5#Pfob84J%($PR+2a>9g+`C_;{8TSbP{>(&Shdeq={R<0)RglH%t@6k-1huxcEEv zlDp9)f8NtJ5K~7-N6-ItaXUL28VJT6cWCbD2zZM%h5$YE|LyxWc6J6{9vGQI(S$Nb zW_HgRLL|;<{+(Tv2MaByAh3a{(*h_wz*|?x@<~R3%2Gqoe>rzGnzwpYwvcr8*bWAR z=~lm;2MnR=U9a`G;J-Dz_4flpxb+j$mba-BLeEopv0H9!)o?3XNHr8&^b~FE?0oI) zcEM=eFlw)p2!K^DtC%x8eA(!8ay8SxAZ}kQ^S( z0=YfbCHwz~`s%1A!?*ttQUcPQqnse!C?bp+C8GrC7$Kb^Ej3y~VWc2ALSaZsDeVA} z?rsDrY4|;Uzwi5d|Jpe_XFJ<{KlgQC@rjQ=h#^=7Fw~u}u}AKm6K?@~>&>>s&05}V z$imGJ_M6Rj|DKSJjxZGJ@P4@b#`WKn%xp*cCuKop`htl$y($6DY2*0@Ku(iKH-g_> z?JJj;?U=&?%6Vm77MSdD=-~hYIgDJcPX3hbTVD=#K2x4GMX&r09BkkxYjC#szo^lV zdi0s9i-^H=D@Q8rKB(D=fCIc&wGAB=CrjSnb)I{5bx@KUe9^UG@%rX`Lf8=TgmfGb!SEGZAlAQe4&HQVG)NtCf;s(k7C%*$;L`5OGzwAy) zn}yCYXpVZH5~@cKQClJ!jjV4s2LY|xOCS|U2sux16A^1ibo2c!5xFf+?b?V-i}Z|8 zw)XM^N6R$^WM#r(5sC%R{<0trzDC@GO){8cS=?7Xd%QOpKdf1!ME4g0>142E_ABAZ z3>4u}@_&BrN4Sc|VcDU6TXgD@m6TaQQsQIT*Q+MEx>SQ_C$_1E|CbE$t^Zp3n%K(D zb^wjJZEj}9vD8l9ZBdw;!-6A>evpZ|@p>-AFRpo`q}WAyyd-@^#Ax^mR7@zh3|~^l z9QS(sO@IZfz|#KvB<9f%zGf&T+3i=BW@P>=z@CQj+|9uR8Q?tl-Euj zNsdSE>_k^lN+g1jDU$;kdHGt~8HLWM6i4tVKOSWsnIGY6)r8UTHOQqgjH+pX_guT0 zv-QNhP7BCGQ8K^UURXs@TZkU?hhKdBmrjpn%i1TDYP}r)_ft&YA8iaZWf}<%Dv$DI zEo84Hd1{L25iS*##t`%W?+*3bcNM+6-<~EPM9t;tSNcHFv4c2NZk7cz1U z#;Gu5F`-_yTlZ7iU9W852m(DqVlf^F8f{W5j)me22fF*oY(Mt|uru1v zFW661Wp=tMNILlQ1P$EPJSaZ2`Xq{a&VpG)^NHwyCc#21=HWZe;hvlj(;P`v*Hb^{wURq-g&xFL1z|Y+F>kebs8;{{Ah=sU>9RqiR&J4Clo`0@sLR^#pm6SKyFdJbTip zu(OX;|488L!S6VCY>DQ4uN+oH&fT-TPVNU6L=G+7)UezYq}N*Vj``rFI<&6kC|Q_J*pXrT1_N9EkCkKtR`Hd=GJL#cy)j+e?=iFcUrOJQ7v(B ze32(3Kh$7e;MwP8p#0-J`}C_v_3xn-J*KDV5#Y)UWoSey~mj^7dDcq zZH9ES>i@0hz4WCZ`-{^*g+b%Y%f`Xw|JK)E+H{aQGLXtWyEx#i0^6!RlEWf%c|8=M znC{L_*1wT{R}CcT%*L0L-66y$Z6;5|@?~;PK#0>1`P}_`D@~phjF8B;L2FHZ)DW%z zfY$W2DEIf|$YPig|2-_zt1Y)YTG`4(9GsVrUY%x-`UsF(^r^&RhYBQ}iF@A>`|xBDpa7H6HlijMW%VszuH z>&%sw_T#>@A}Wyl)=ST02R@Y_MKedoSSi&+|J}lLxh}h&Bju#O#)rl^YqiDAz1N4E z&))$hxj}cluo>~_@X;h;> zVl>1}uXT{aMCApKztwt@(zlf49vjqc%*N+k3}3DNEwp|7*V4~n0PP8Mh}&Rd2~wOKIAJ*IlIoUHy@82Uud7YNP4y2Oa)( z&3`A5O;GzDhxjLgp#P0asw{E``Tw>r4=*wDC|3OnU`(GAyyoMQnHMVQHSW0;=W+;P zQf-+1Ciyx#yM7=jh*j`ep59j-^0vH0oefn*Ie21^6-Q9Y&y`k6@a}V5f4(E?ek!~) z@*hfqG;z17wi(jb(tH>i=kcYAi|nx^)rZAHpO|%K7VNK_$Nzej5#;wjb^cn=ddBjM+2!9P@&+bcPUH&ThXy>eCBg8XN&$K{04|n5SDt>2C zx!k*vMZ1SpRM7YRir&atjpm%&0-;BMYzTq$nz|)sVJLy=wY7^(!U0i_=O0tsu^U^) z%2p!Nx;tzSb<%#wpXt~C&r+l>kB-)jQPcv9Aur_aqjGNT?%@I}wHmxX)N@{ICJEa} zZH2%-lclFA(f;7k+xU5=(CVD%aYJI&xBF`|Ljix0=Kplc{#8% z{TpH8+q#lPGd*|cTbC9n-edTF?`u!*6VfMt%)uUcz`rQjM+x0B3Ou2&wIy`dJa+4m z&+7%iTZ2OnJXTdRUvG=@onSCMd3C=mX3L`jpLrJcwo?d3z*@@NGi^lj}gySdH?JeP7`zolJzGa={di_-fUBWux} zzK7fMq+YZncPD~xq_3qooc(g?>3Wq;@p*uEjg&z!-6&DPf7euy3?+S>#x8lpn@j8W zFLiiuFO_J+$1#LPR&efy#=r29&F;|y5`W8)s7r@ugO7LnI6a~maxB?Yy+FL)yG9@i z1!8gNuFTz=PS)eTq(6>|7Tha4gb8*=z*qHn;ARWaa#psc$vd-Ep8O{Uy@RdUw8Ak`IcP>W_BqruZPwB@4PA=ZROq1TohX24CVi&Qy2=MY=^<6PM4k{3h zs<+=0v8npY46#V$(-A37!k7>$O0Xt1B@v)_-)W@>L(tiaVnB zl}6daqR=3x?y^HLRV5m&HPX42A+sT%C0j9%P$HXQF${5FXGHg4b#IFuVG#TH= z1Lb&&+R+Qt*m$npj0a-Ofzpuy$J#T+t^0uR*dE@q*oSm+Yo3ijwY&?xy-;}@geHAh`mgB{}dGeGaNQESdtm{zL3BDJJ}*gjI)XL8N#(h| z5eX1VY`5ItO6Usr$_Ux3-NBqDw{4I0;+>{{%_2m%cbl6wIW}W2d0@r){i(qoTX|pK zmfcZQrmu|WQVGT?+1oqI@jde%ml!zKeBPNQ8FBQhI8|GVMy?y=n#p@3^UB*_DP=uN4Rr9XB zM3&OnuiNmijN)$7d0p1Zxd93+l@BNzcZT~qO1>yTFIsgVU0VRH4ffkR4=}AST`waa znWeq;_Djy_v3{sjVGSv2xSsPtXBcC}KolQ~(nRMSSlYA~dKgj0W0}RMZ5ho%MAplT z+QQl; zuV$C@JI~Yez1W}p)PtxabdJ{W)iC&G@ShB-QGDg7`@mg53F>W+@UmI&*!veiAKc5L z7^P0${cSEW0ti|?OuyXIri2n}L!E*knJMiLCHlrZ7cdbF13c<|kHv^%3e~B^SsrUs z{~Fq=WM&3qwvJ+&So=ky@2xyhV)_F4EO-7UNR#G^e}OL1O4gG|e5VxJ+zQU4WTs#D zuXG@hZ%fle`xuq+ImD=!-m;gWiF6cSj6B@~Dmu&c>JDk(B=^S%xy5neqx)O@MV)HQJl^5LM4H1~>*Tn4wc|?^ zu_G~mxG$1)Me>GVeS6Oa_ACi7rHD|t^_X>)%LuMHjE+~z>(w#IA_8rqAEyTP)XR!U z5wRQ{dmLdHUjDWjXK*=`vf3#|R#f!`YplGU6Y_qOYg%77V%BHpJaGOrXl8JHXGu(~ zBrR~1!G(NctGk*yR-H|CYdvTcA6KS>1@RhMk)*_G9v_{*rdsXhy$)Pb6q`Dpfnt0G z#g39)U-C0pq@*jJ*KQXtp0vJ^k?lVoC{z69SgJ`&e)jB~ahT@bkDp};c}b=XZkw^i z_hdHGeu*1&vvDfYRQdz`01x{vh+_BSfkzDXi#A4&b2caKv|D@=e_4A` z&2K&xlV%}^ZthqL{}6)F15v1F2V(9de!=#o=9-}N{A8LJqWi9FC-5F+4RnjFf#Uw; z4+du+C)UQ~dHxz8$a}E3hv3;m4hV@jed9GFO0eE(Xf7ds=z8`1z+u!RXOkmu)E2tL zqYx^gwNo`RP?Jxp;yvKVDz0zyD?P*5^sO9JZ4BD0iGs8m8MMznU0uaLOv_}b-(zZG z#tm0PKE($}hPoD96*alJgk!-73S=eFpwJ)*^Nb&IcAnMS{KZ-sC85mpHH4<(bpJn{P684 zs;gr9pGx;0E?I~+hvx7^58;^yzaJk3rN56MaLC@Ng1{yMbi_+GIAFpKhqw9nsi`qd zyve~+2Z8T<^^}&DKcKnAB<-luW;Z&l-dz2h%}@d0GhO#jQ1hNE7CnmtQp+C1)#@}1 z)31s}vKcz&GJ1o!+&UfZFa~J54Ri8RtP9JZ96alUc5jg|bWwvGY8&_?5`_no>0f|CF#MMpi)3ERE5!0-MA(_^f%|~Z*l)1m`6g;Gi zq5Z7~Gpna^RDi=g)WOS3^ULex)(CBg>2na%v3fYjqlsimsoBlSLu@&1fKeFFizJQw790}9zi~wR4ho8_mEf8&*)y*Cn6NJ##ufPJXl`KxY`!9 z?km;oAyTNYi@SSW`g1Abq2&*1rL$w&I|ahAg99Hd*=nUZ{7U-5zH8HGBusx-pO$x3 zVRl+npMes++aBIwU40Ldq|zhfsfO0%x#PA=+b4=zeSEdZHa78#o3GSyGATNpRyr+# znfg<6b7Q$UTs$>2vYO#D;SdPhhN~lH10v;%noAcHK*!ey3S^Vv`0*qPw@3_^djDZXC-ZD4?kiBljYa(kk<*&z9q$ z`M$R|az*{vtr!uB(incm)hK5PiTo=lK>F>T%G^5{npaHnULpaVubeXVV|gg0_=~q) z4Kt|4uk}D(^%z_I&{C(Tx^mCj z3D^Lw`!8C+);fL?S&~EF>%nlrVOdj_lAE$74FB45YsnmC2w+N+!X$VH(T@vq%8|z3 z>w+uU2HBn?o;f|wn0?JV5j-b80In{3z#4S!gNxe!kS%@ndZfwy;{zyusk`SJ<}d;7 zr4x)t9XSAMGZ(yAM zSiMg3xB#7E;%Cp4tAU8~pEzBrQNX&uoDh4Iy@MH@eLRs4dCJZ#REU18*~GmSZmZfe z-?3lfj`DeHvpDX0V9$w3ktX?WFNjKoK1qi&Ltj5j4?P$x*&zEGygD0$Y?jdKx9h_? zMWU4Nr{^{d!`I{J6b2g|o_y>U=%L6OKs^Tz)tY~r zsVnl}?%~l?*`w^{^NlIB5Q*ozV@rx)E%Kh4i<9cK$tYC^v$XZhQ*p*A{*a-17OlBw*9rGV~Yz@any|YPh--LbP6nrA{=YAUP)9*~mt<2jE>sIN` z<$$RW@KAqFiSY`Ht*-WZPL{z%%*<&j10slRrZ{ve?;KciT&1cjX7n4fS>UXn6 zXa-ZS;zhWU%TH{{lH-_X7vCe1_h0aWttiO)ht&yn zIHI$%=zKhlHab0`J6K4qIKd<4jzn8)>+<){C9m5hZ9=q?__F*qjq`dwC8D);G@0_! z$aoegwa1v2J1f34PL=t{)jbs$sGjb2W>0h#>FAmkk@1LCgZP`=2v@N#IZ(yc5BzkY z>ZWMT9iTI-2UCqyW*|uc|~Ye2#tUD{qkCItt-jm z2QWMsd3g6Yf5HkQ{x{k@+GM=uecneZOTzx{l?*i12WVlDBU45s>vqY_qyNlBeuxfq zt4DD6rRqFxouqmj^K<+wZSvUmho#!)dWo!o+@N)TaHfQoMZesWbM^G23rmApw&n{4 zDM$N&BqUbh+qVIlaW4Ewy$?~2e~087gq?$9UKQPYg6D_noo$D*b^mmkL2p}E-!BrF zE=0Svo)@EnHfBP2z-ALxG=UKpR&H)dfRP7nu_-Hbul389vB6&}NsmRcqyn0aj1?%} zu9?TM4w6fdg#msCn!@?D&p%B7k89G-oj?8jpMc?NNnUVM+TnNhn`cm9vW?xHvTxhn z6S}$@LcO!k^;a<%AZ`b^8mGw3ckDN%E*nqI`*fIj9jT^(ZqMR7^{e$en|CI)> zH>K3&*MtgCP;&BG5y)_!V<*+87@XY!I3ahozE95G(DT4txHS>w-1Vy>wBO#W@x~3H zp(5xi>{&B6H-~)St@MU#PD74VB`P_e<|RTb1%bMYoPWJ<7HQEK zJkSwjfn2@4>b_OVhKV@-68ie*2n-+1EH)OPgB1Nr@4)Hm51c0_0Uc&z921i<&COZx z>6w|-Q9-)6QEZiIBh~a>KMPiYiJ((JLZ*EG@}B&e08M(V~`5b!F)poW33jF=j0uMmF*CRJigZEEouK^uPSIFApVmmhY zdg4|RL}|5N=c3g04I6T8v>=8#y}Sn^kS-eM(Sp(V>3pnGG?@FnGM3r>Q^%t=L!rqh zcK|Wd+$<8-snvmHZ@Fb##7up|HdTETDa4jm*1|8Obw?=L&C*fT_4V}IU_|il!OM}q zKh|0kwq&R4cs$cr_kxVD0#)8~YYyt5aKGLU8!@4_fljDGpXYr}Ra#(cKQ8srPZJ#X z%!u`@aa@qD9;Hf|i0zL!=?<_GkyoDLS}PzisUw-tog_2bj+LYkb$q}5y{t8d-&4+# zua-bAGqFU~ccs9N)z2UOowRY8xw&q;y@%ZeY)e1k?C$RirT+lbR7DLspyyARO+O!XUuA4~pDJEaR8HKl50apPS>Io|<1Ly1q{;!Sza)OI%>8);q{! zr)hAG9^-;=%>=Zx4fs-KcL0Dm8_V;FXt!`h76&+Rz+fSsZ1+SNIc+kz;FTqsgHb)5jcm7trgV*ZsTrH~a34Z)vdG~x_@k2}|QB1Co4o?w9uwa$WqrcKT)uuIDJ$`#t7gaB+Xl^FQUJiP% zT-eBa?up?Jn0!{?oY;P(MN<;%(i+V! zW<{J8|4LW`@&zMV^pt)k>u<5yUd$#S=>k0l@G2rl0B18Bo;kDuJ_PVaRKrrO2MN!L zsYb>J-yl5=K>)r2@3RAZ1ZzEy{IXS9A1n~lcHEhWT{rm}O85Iw^{4ka_sok`IF_HD zNJON2Wn@8ENc^lV#QRu^my&3Bv+%8@pg()#(p0_2=>>&?ul{r0z_vJ!&NTN{U)jRE zdX}VoQRgM^zf`Qzf5taWl5bOoJ>x+v`?|T&BLPRh6B8g^PQu3quJymuujNvlDK38Z zxrqn&Suf^0d|GtJY@#vE7&F6*d=B@qjVeEJ*3KmZiF~A#OnFQEZ4`&(1TR7N-Rzt& z@)=+AN$J~WYVUYT+k-bSQUc^N*r)ei#9qCueqKZ9@!HP#v8D!okC)~V2o5(B&O@IK z#GO%TQC&}|qXL)!5iM@Qa5;CRt{8UGtgn}{?tP9LHHtveOE=rf7Q_a9K zqlV=)W+V}s&~=$2Ue9+~a$c{tBUnZ5A%+#L%C`3gIaD(`v~807KRf1J>FN(;#5!gU zk(jPywC^HIUDU?H=4&b9pG^*YsQTRTOR~sNV&f5h!`+o+l zJN$9bLVjyNqIQrZ17(^Da*j&*!o`G}3liQ}?yL~k61BAk<4}ibR;G`Ze(HE(>A8)1 zCCMiS%-HqzKSZX6dCR+b?OD51QBIz$1%eFRgS8p|Yb+=ZHx5?uecpWn*HEKkPX$`F0Z&xab2e z){Xcf62?LT&l#veHA|BT>fo;S4&!}v=Wky`_+=;rII*98ZcD^ddD}u!7$p>!B*DI# z%OUnd!~j$=t*XHGMROMPr#D{F0}eJ!mh{#QTF}Y$^>_^A7C{SATGUQkRXf;k3v0qu z9Nk|fQfab~yn5c@Hl&vJh)4z3src+DPHrf4o*NE-n3`*?*Jd8en#g4huKBa-?xl(s zCAR3gyjL6^{zi@r4-Xe4*z^8L`)u?tGaL`2)72^{m1_Xy8=^wj-Op#F^nA3rGO-XD z0;3<#2VuSq!eV+#YQrxzAukC@m+BOwjbI)|C-EH}y$NYC@o7)HFw*oxg~+!FDljOI zq8lX>rI7}ty3@K_IBpc2rqD^^=jC%`?I`GazY1JPPDhi5uO~_;Ca6ngX$pEq%_q+5 zX^RlF`@NS!By8ZA@3ZehZ9m9#fY20%)>KIEe)v**&#iNd4ks2<2j9MGR44 z;y1{ePg}+pG^)W)VpXCX-lyO69^2b18s1`&Y|KRE%L(^otbVT<`VRYS0xayQu?mCX zk0cVWPk)w^5##ibOr&LD(+iu)7=G|Io;3QShkn5#H{Z$qy;jD-9VG4w_H>C0!IPEc zGH<6k%E!zyb7OllcpIE~P)wWZkiqRWD1T;0F8_QfmnuoBDD6B@ek~`qPwat=R-N9Z z=7dC9XLqM(W}$@G^M+PsCvINe;tbnwF>hJPS2C(&y0txgjp}tVPnDcl7pZu9->JC| zQ>~bPsjzz(f+!--g!}b;_!TS;D=3{gBYSdHhqin8@?0&w{lI+~jm}gdzSn)9sfTU; zUqAm4W1r$%{Priz6M=_nepB;(;{{o$Wd440{X&x?0!W9{AVDYe!N9Eo$UXz!6hOi& zXjER86IUzLa^}?u6QC&`*`s-hHw%_dPlOZr;Ucs9BJbH8;|Y_fBgMk9zirAW-VSk> z9Ujh`fe$OYoQGS_z8QfFWJ6!n?`6D8n@DMx`d8mO~ChVGuAw} z_Sol|^kC;Z`$6^l{ei7(iBsc(V&r>ss5DKh5mr_&;BTb;8Y?%dud8#NZRo%PQ>~)3 zB>ARyaHmXP`8?Iq+Fs`afFLrk;b=c!v-mM-zJB)cc@Z-fFhMMty`KLa{?&IUj;B^D z3r-a=Q2E+J`EM_w;7_@Y^3g?m=vZPTYKL=_vu?W%8e0+aeLfHgUbOD z+L2A1K#g1y{QT1B6Ug$RaPEM~4_>TEV59sqE}aT@M`-n5_a=;KJ``3|$ ze_f7H)#Dp!kxzy@%cel$ElN>B0j8%3)uiPlM@&00pB`hyEPPLM$|taoi~RF28YZgx z;=mZJYs9%#xY)FI;wF!OhgG7NhkguN@)=5sUvKRnqnLIwG_OBJmH4u$YMJ_mjXG@nwX%nuk<34i$q}Up!9ZU#T$Y_;9C+;VOJ{DDJ0>{WByWwXmn>(${8%v8lrjZ^SJ7yaC| z(9PA*#?krhna*tnvK8>XsdEX~7-R*7{2>f^>veOza2wBN)N8$XMFl^s{R@youOU|=CmIf`M6xMwB^yO0p#FE1z9^nQ%ic0F!B3G(v`u`uxS_02$KLP3}S51S0* z?*iayhgT*I&1!1*xa{#LiE1e+Nby4z4tlVOy}BLvZyA!+N60%mpeZ&rvwVrB`uT1Q zhj?1U*GiAtx%D29gD55F849zN13dK}zUl4QODjzKI>=f^Z=}Au3LSk%{!{)GvF@B) z2$e2Mi_GfDZFmaZsiIEa$PKA;0aoQJ&E?)Hunc<_Z0$~8cohvBb~>CGo5U#@#d#3e z*_T%9zVghM69MpF@&ko5CE~K<4FQmRqqy@#<{8Xb7js@IzrI1RVq@Ts^Ua1qL#rWV0wg2iB(a?`)ctiAY zkgo-A3sC}U$W6;eZg7R1@F()|-s_u=*5*&+FG0T~_vDB2?}guEQ=4pQV#YZYo;=&B zv5**)c`+hPHC=1~2Ny)mwu}s7C)=@FT#%wl{~7UtvAM@!)0Wq??elxCu&qe?JVF2% zuJ6-m`Zp+KXIvHWw=><<+w8O>dLQrX&&SgkfFCozbty02;5eQ+6fbuL?|x5hWrLS49EK$Z5Q=0$j(U<(2_#)%Jf&|&}$xkl0%^3Dt8b{1k z?u@(4R2UL!&43JHJ=R2gp!OUAQhZ_?6_1{?c)2Y$-u!m%WLrN%W%;`b3VZUaVM8+Y zE^r}?@B4__ed^Trv)qzm4NZ)XDtyOgBN?H8{!kK+INl-uj?}t0tK0H9mp8gu56Ptx z=f$pg&)-&@vw-3=`gJFMv{z?0q$u9wT=mOaMkNpefQ?w#vlb*pl;J=LnqL)gMrmgF zir!Vg{yIe=XMD#`Ww@<*0#Y-d8`@*)O2yPi1~s-^)?!`xATIDGVtQ6Sh@R zqY7HnbV|qMJcPX>w(|F(&~NUXaD`4{cL`c`MUDmsUQBn+l%XM#$mDrSKt81PQ6B}* zoxMFQegSTJ+F})ceTtZ|2<)K!2F15^ml8YpS^qf`InzW#X+>#Go*rknP^sbTM}?qtOlf9* zhK*&!(KBrzQm|9ue7#b4N1!Op%LvB)CO3_$I>B+~F34IZtpKRiXXR#F(oA8FGbo}) z435oPMqfWE0MG!TqRPNYnKeUY_a%4eYmNbqxKB+LVA$k6c~@HmAA;`9M+f%tZY$$i zA679xv!3iw6{?L^ECMs;)Wsoz_tVEL{~g_A7^0k{jsStkz^jC1#Byq zCKJfd=yEkkxU=<=` z+A#8~hC@PRUt~z%>-)eWt+;7=Ch9fYrK@{oobvZMRstphNlTB){85B!24vJzPnAU= zy;Fs1(EOt{Ifzz_OP9~9Up+-BMt}$R!?c(bA`@;cQexBSi zNxtZTZLLaK$^{+mrjd8upM2HxxBv9|Egd%CI2OUrjcc~5=s88%5oVhW+fDuQ{O|65 zAEfK9Z@0qLLUt#tz3Ipl=%jo5>cu1M#>Lwo33p{Yjrnq^oxQSBbttO}-2v9e)*roG znSW^SLw9x81ZKLUxw+p4Tj>gK7*Xjygzhu0l)8EP3P9TqM=VR%umxbs4W6%sY}kQ= zH*);cGWS~!!&9!;(nDhGik$`m@7f_<#og?8TA6urm7o>emFTMXtf5oRyeZ2?Eklnf z)l;Vd+X5PR3H)NMBg}8^@87!6z4_L$&jN@eRAGtR><4b!`P1t_d7&cHX5WM?+bNox zD=~{LPx3x9B5%5x7tq>zj2!ZaiG_s)sm8S8FEMKC-G$BHZZ&76=u>5Hy|>un_hj=j zS8Z8f?>M!7!7+0^J$P#en=w=m+iIT8nwlSbS#&JrYHG6%(1N1jmu@$QGu&JK@Aw;^ zLz;GD)mHY(%o>`FKW(4NHz`49W@fNg$Cx20t)pGckoLElH28@4o?z7V*aL2w_8Vu( zmy3vwFIpwrv^5diwlPz4 zq$GYsL^1#TtRJqY?8@GIk>27r)_9fPx`2@WzT_UoV&*`ctSAy?-9f=cUOJ2 zik($Er2bUvWb8_vbFKKL-sH9Q+Qp?X2UImC#>^Lg|uIejlIVQ{Lu&!fw;viqKaivhDOTKAt1 z3xo@FksX6DR`4IB?kGO62C*83Uysubwamt@zB` zl))Ing8-EN$wH~K&Me@821fq1cPJ8IU|DrDaHUjc@r3T~4Vv~;0GLi?QB?xfej>3F znhgIrZ!~Py#uur^U?8Car(L&Q?>De9r z8)T&6(6TdA9UVy|CJCGz&&_y3PU=MRVW@E*qI}0@g|B-s{$F2QAVn`@Q9fp6 zXaL;GfR^Qk(ujb!VVt&K26wJ^d(C=EU_lu?O9N$4ZxIiveOx4C@OG+@-2h+-=hS7_Opz?}Y8nFOc$54= z0hXUuQ{SAW5fh{8_z{f;*|zL~Dt@qvg@eTMve{5gKRK!l$v{|ynAl9)XX-bXp5adi zi}VbVtF)Vw9Sn^SlIp`)?WQQ?``?a({(f$%UQ*>R!DyNL49s{op~9o0lgHmWv40U> zpv`fYy=nHFe4Tf7ZPQd;*W<|r@=76F!=_NRca=#2<9oPd}JUeSZqk&mi=>3RP8_0N_ z%llsy)5e+Exw+cB1|!~TRxRdyydz7FY*-K>ya#l^j1yXr*p8BS9jQSPgzg{Sf8>fm z;rRox{i!Tp4JWR%zVjoT$tjf_l6dtyir|8_>J?UvkXwxwzSZrTbI;ru=GY0LDVUN^(=*0+&n zrkTBC)AjYtGn3ttbqB-5O!TheqGIA6K3?l;PM{K%>xbb zH7KaY|01RKXD96YNXuug2PvFQ>vd(cJEt>GXv^u6Iz*eiACyKBck)r zVWc+Pz`#Igy#Rq|dH>h)1)0*pI%2NWqWot(vUiGLJVdG+>5zNIm^?s%3yxEZA9EV( zWO$7EhP7NRx&{Z`IC=8_FBV_`VW0)#C}N`QP#eRav;tC9k31qp+jTV>P%r7V7UY4o zYs#`Yqf8t!6l}v$a3hYx`1XFSGFg5Gt~>2ZQp&LD6iP~5$$_Q@&ICwkEbX6IJ9vC) zA&dm6K-9UN+Ymcx6v!`v-`(De-$7H9v96%-oJe1T08*9eiclef#@4#Z-6iepq9}Jz z_)(|f^RU>uUBp(8=WeDgzXXnlrEbgisddwV^8EpuDj3YN1zs zLt2l_H#Gx?AxoFY08$(aGM&Iw868age0=@(s%=EGA@F;$xQntE(5<9{(|`-` zr6OWVAuflxVVosI*1H@v4n+ChW+WXOIXbd!d(FtakRpq6S}l?8(kL1{F9Az*s0fV$ zktwJg{ot3yb&gNZ_W~2_2J%dei1R{Q8QK9YEadEp@J;I_jOB zPCUYyF{|Vt1y2x{c>0W(Sl#fhYBV*^3e(ze7W-MXuU^y3)Ha7ogxjefkIO>$sj|v9 z{buH8WXOHmO38Ms%(?h^gzh3g8mAdKpK*GdZ5YDQvcAP=SqWd3hphu6BI}CX{Ka{8 zZ5n|E9d*f-v)QIAvpr;pnOELfn6sFDAdLMd!c-@)H(26{a)1nDNOZ##`alXdZ5lo| zUC~5uIPbqXdl)1%SYfN6GrKyO!OaO$u-WXB)`{);pHIt^P`1zbvdpn7z}a#|ik zK0g;A<&3D6$|~ust{UHw06)6DI_ga1@^QiJ9=`TG_+~50=_$gWAd;ISiGFP(Nk434 z5i~~n(6iV&m&@@NZtvi&XI(*N6`Y>7b9CDh6fuDC6G81PIHMT(#8==qXwjTgbBM9QhMXSA z?|gmCY3*7C3MooU@jG;u#0#x@35|f1IxR*Z`S{pD?fcjBbMXE^d809)@X0tI%#lM% z=L}+Z*K|UtK=W7YFaC)MfC^Vj* zmSUj`u(>NKR_OKBz*bL$SXF;|*naaUa1rL6NOWldV6z-z+kr;6=U`7yw2h%qvGnqA z`55dAN2X$5F4Q8M?m(iB{(> zICswU3h?%HFs!m@BPca{0r=t33?neaAQtvKUo)@$`Nsw=uMWVJYjt`C-gzYoK=-Qx zjQ}ev=ICoRJ0m7?8%epTq(-O0=#X#-hzQc1GC-uIl?Dmv{$2O;Jm2^I1Gv~e+s-)7BNPbAOg4D_3B14H z2&H)_6$4TsECRo8`~oxF3%xOPx;I)1=d71gno;Mbgtc%hl8Ob78Po_iEtP8wH((#h zXYcKTli{R+PpsAe!y0AEhx$msdj@;}c!<#@j|836u!RF^sCPPmOGm2bO=?we4hHZQ zG(*0tI={KpM#RvM*7r&uY*qX-9h~kI^lhHu&SF?}KUq$)VyZc|HaL4l13Eg;Jo{lD z*tq_!8kmwTdrtcwy8`X!tb_bMKAtuW2wz)Nwo5HK>M1E`ABPArubTWjO5S=~1zP&u zre;No&q}H`Bl9-2|GVgSA7{F1XQ%)E7%w3NE$dLWl~~2+DkL?xmOW&ngAC!Zw43zK zNw0=mG3!>dj0-$bN3hxpaT5=im2sOjp_C>(Z=z%-x*8}$tzSA2n>)`F7+zC zly6PrG2*e6y{xK=4}8ZXZYNHqY)SBHU+W7PN*+f{A zYxW;10h~!!a|l4}0&D~soq~oi6{(%r=cCn9GaCAV{MJKH$?@rm2Es9z6QX_sp13f> zwmS2`hw{S=5@jGBR5G}qhKGYw03!d5RBr6hB^2+?vBy6!KK;i{LlrVKLN>bjy}Sl; z;xcv!cNGiV6TpP|WDx(TI6XDhu&ZHN<`(@Q@N|80cJepU(Z*4VX*vwDvmU*lS@ z#6BV#mdKLB7NHm)YblUtK4ni(ad|LGH=F5@{*Eo5?YOcA8)HWx5`#|^3bi6Ev$WGz zi&nwKC6t!mf8vt-JMgDTAGyt4twCVGQ%KW!(4Aw5#KTL*(>nh+Ie-} zW|s(AMDmWaHA@LgrmfKjnyF7>U{H*ps7B(An8u&o?Ik8wP`bfCD;q&hz||?oM<@js zz;TKB7a!Pw7L;>U9cK)Uv!y0J2y++qE5kaYzkwl8Oy<+#E?Kuw3W*1ZIKf}6p~d<* z+1hUU`9_6`xVHi<#WXTu>{*17TWdo@mIOYGf?o9b?bQ)jtp30K<&ZPV*gNQtOOgE7 z^~&GCZklhO6G&(}EX_r?7_CbbmlAoQ0G~KoEkb1AonL2Rg+3o)`I)G~B}m=afgVWR)g*HqdM52)=&~ zbt#T3d1~zlnWyi)yj3+kkEYks%}c(lv7#=VKU=A8JUiHN{au;6vuG-F&6^HIucek@ zAJtkZfqt)f2ovgHdM6wg~e}A^ncxV zjbD07p?PebP>{z}hR{<}rfCmP%ePU#pLroM(_eP3(5fX2D}JL!HBfpkuHskAed#HC zqSgNPq`vxUEPr{ejFIGat@UcfFDp>Vzai39iY3+uA9^vvkp5-J0$?l$1?7x7GM-8` z7WkqSj1M;HX$kkba`lPA6u+-^NFOX7?2E}rSujLy>^;ic`u0C{;(xYi2qggAEPeBE ziNJ3s#Rt=XnC}aMWI?oG!XEA?Gi@WIVpQa^NnYCf(41zmI`NgXd*S z_UdGXN#vX5pNbPd>nh5J*nfK&6Ua`*8=qv2v}_gs2@gltxnmBLy@ZGa3PA-1MY%(- z@3Vy`>ByrHrhEp)X+2g2SP^lq2=;%4lK5I-vV=!2(^x;R*HEa>UNB%3Rb$AD;Beb& z+;wWe4?)dCMvI@PgO-4QYBqHp)pmB5Eu2xzMV%{k%H03S`ql8?Q_tb5mV?N!J+eCU zs-ZXn?4yi5U3cvYjAkiY`24&X)7G|`?OZBPf?vJ95*`sW4Kf8!-#3^@``JRtg09l4BaeahZJ%&2#W?=9kZ1j+VqKTlG3q3_x zVZ+y-U90q|pQ&Pr2)2%~JXCC+8Tzov`ymUh{sy(pw>N)7$`nDY-7#Px5Ofk*f&G>n zSXm~l;n{3HcfCXa`Dm+)cUArHlDuRyz4fSAldbh8z6N=ke*4|xe$SsG_`UGt$Lcn$ zP)=`BfN8B@cX&ScAsqaJOWaqc9tNyNyot=6}{@nt-R^K4s1K-2g2_&_B;S z-HHE9>!8qL#qL--e(3K*!1u{}{$huBz1ff#wDfCY5LG9(_~%D#+h5!%xu5Id&xVy` z$mq=GFWH1dE#%<90RsfsCElU=!XLR!&$of`b8;$Xb@VJ}DzxbC3P^Xo!>39Hfw9Kv zp!{|lTRzS5@8=6G{>b}xo^TfG6)bOV+LOw8Mhi+V9hdGkb6U~8=9@S^Iy)103s`-p zRjikn16cB&^I!j@!|Q)#Qn^_FK>qBV#rge-e+D;OZT7`i1LeTRLe~lq*gfhD({ygO z+in+Gf4g2!0y8y#;DL-#4G;&I7`;)PE)$O)T>XhN4?171b52Z1P=ihJS-8Xk{)e2v zVTT9_u|I4$I0JPQVUBmZr=N?)g9AuI>2pF91HZh(G^mtMmLGva%Z=l|$RrR`L-Ni*(l;D<~(anaGZ(-a+?1fvS%z;&oZSI`GLg~etxsEDE7 z&bzIiolUdt#FHz#FN9$EXzxOGp0E)j6uQrc$8jg8r^t5#=zhRHbYj)xC9&CVK()x2 zgE4nG7E6|#t=NIb-qW6psi=gE#*{^`=p!1v0ZK>lp5y0EPrvGa z$Q<;R0~Fd&hMi1(0C_Hm;@kLKRW9}FG6{Np*xXOrc9M6bXqjas7@T`vxwO|il51;v zh24;KgM~FU>YiF55LOxLTwx^iWomKm(s}ShC)dhrqZ%C{Xl2%Y&{wl<@mg#q8@$gj zacJN1y)zB;>^@tsN_gr(w>Ctv0=KR^p-uy-HL6g9ahFDcfF)?VF;^W6e)~QS=baPa z1MSQ&Hq(0jn!a9*I{9L9Xj@$TRLb*D3C4Sy98_u>Te;m>f(OcD?&O8;&9@MMl$9wl z=NMa;Oe3Tgo~39O_+Wx@3UctP_Hhu>jF;BpX3GGv0$`v>|4kv{!a%}i z+dd-bei*(ivo-1ROqN)Nud6gW8L&Y2A1rr8k?~{-A`o|Bb%F%#jn1Fe6}qL7y6OlP zCKdpHL{4mU-`EzO{~^{&oZwyv-q>?0VX|8aj>|i11UTT317J)Be$`VaKr$HIadvXX zUQmgjP(n%yCt=a}1_BDY>I=?B2c1O)PoImSPJjf=cmKnzg8p`Bsq4TE1iJTH`xYWl zJA`i0>Ph7UF!RNcHm=1 zWe9j&y=7qr*KekCr?1qi%81w!r1VvXR}kMVt|ecM6t6!=b2^SHO4PPWhZ2SvKqvW8 zoEZ+wbe7byEz0!jmf_)LJb==>BM?FY@_5$2>2+W|oIB59)}c?VTqkD3Xxb6a9*Dfx z9Mr~c8hVlzR5~)uL71#avzyH)@Mfy@8=Ysf8<RQeng(pl)v|^R zufcCXgcG&eINpd{)pn);VF9WT*56C*5d5@#4lee8VE8XBtTxUDJS~mr@0MjS*CN>T)&TS6Xgb!u-)PpM5wSL>m4Qntv=`d z1kyvFtRAn#l1q@or&m7!nL)g1X%B8NyrvWr@t zG55tpgNX_5g!aF+P)6H*+8_aK3dKMhS{aErHviRoeRCe9=`*)_jkkOV*NKr5Ov;UP z$&)Qy?s}o;e^JF4vS+4Cl`XHH=93L)5;MEg5L z^fTer__ig@*8%4Tw(*HjxVI4ZJzLJ+>AEK&-0D3kq}adEmPfK~)woF2JsCZBb>D&++EL}wOv32us-re6=l+|IV1At!>N2)ww2Me8qmk7Zk|Rq)|{AE54AuTe|9L z#_7|Zz0=2!Pkd>(vp);M;5=E|Eh&d~Kl+o7N9zB~0yiSyK>;7w3BTfF6+LEa=QBx@ z@IbG*4nt@#uwHs9 zHBllIH|wYwoX6{H22r@2Ji_}9KNQYx2@)V82*=A1@GLV{S4&=SH*X{damGSTo8Qb7 z*rJ5;p|9%<@j#aREJE^?xKWyI2W(hebQ3A?sZR%Oo469R7bz!#tFe!ueqL|X5vcE@CdqQV8>L%SKWeajGec4+V5L zY=o$`@_gWp( z?3}M(8Uxpdv1#7yIrf?AU;49LTSD;^y0J0Hci@8vI&-}zVwL7%H9e#5$-8dukD$lb z{^V~0Jj$>kWM!1Q^F(Cy+=Bb7gLJVST~Q=Pf8)i>#qN#FUC@KxR%4>c$_he5HA+P= z>R9923|gogVGBuZGNvJSGN>0hSfs2hdj`OfC5`ez&^STi246R~L|E&7b>eDv`e5-_ z=O$Bq?`-wQ5ThzuL>g50KJ%bwWrzc3X3Ii@qa++qQQw2EiY^AfG5lbXo1Hmxa}zXM z*>n7L)8^=A>7=~o*#25o`kFIHXekrk@Zhoc5M^Tiy zDybj&N;(g^)t4K{@;O^d(gsxu6OPqKCMMi#s+8-J-2`%h66I9@nx)M-Chx{{7;098 z9~Bh^I&y3~afB5ErnLBni?mX9@^C8&+9zVvx(;{BwUnkl#N`mEhkX z@F{UWn*hkek`8LUwxljX1J_EEH!}bwl~9Ass;1D$F=2Ge*YpwI+k<1j2=v${A>`R( zVT}5aQ-dk16-*Z-zz+8`5-se-O=^g7#ROm*SwDLdJVD!_xa8NQpEMeN!b!;#JOjlLZghq`qG~n~U-FWq2sLlKuo{-&0QRw?qYvAS z8Pk1_JQH2CN^2>slq)sX8+0JHRmagLQ~G3V^%L=5ZC69}k%D_-fARHYOF-2V(<)Sqp_8Zlj^*2Pvz+x4)q!z z#+GVbWr}2e%rF{=vGacRVq(S#h01j!3swA5P(ovS_)8tst?gs0iwbgW*l7H>+w|@F zf=YgRe&!C_il*K>im!uZYN)OEo5U#n9=+17&fR-ZNuL^{I>Fx8wS2g5m5YQSKmbn` z2Cyy{LAH4Om4ueaB37L5A_2HGogXq9n-^e&%&Fa9T>{gqEAgtjY?zNT{ zU9x~c>@NVNZ}dJ~>LAdX)g}j&v&UX_J_jlO{PFn>HFH4j0T1(Qk;lM0$L2D#r0eL9 z!66L*k_`rsY%+C(QcI-mRK*Z^H)y$Yfe*U7xAS1R!bA-8A5XyQ3<%3A^au#ZM0R;q zT5@F;rvWrm9X)k^iw3(G(KFw(mUv8ZLebrou3U_mCowAW>qq_dJv-0Aq47bKs2x1( z_fXmP1#mJcPV4GYm@>0j19;At(SnpP@|ZEEI_Hm$jOgX2v~OWmtFrmZ z=BXBk>whibe|w9v=p0D|gr1d}N0y6}IO3BbxDcN=iC#fZ(MekEiy;}Kh>_FE<~#gP zV1G(;ZG{l&(SPednHK)9s|d4)kd=qfrh^3uguITG46>LUlcM|4Aq1?bTChBKCmSI) z?lUP~wOZB}5XJxF0?>}>koA41C`QU5%fCNvjb_jb#ehg<+nFBOfQZ<2ITy4o@Q4LK z@`U$8I`G2<1d2Xm82X~q;@Zt#C<*O%IzNylRw9()Q?`c~YAEFwD7O;-iPD&GrB~zH zG*xb;qx>f$5ww-_r;5q{UJO%y3MADvMP_|e+brKRXRdtaPbCBCuLMQn(_LC(C7Way!cTaXD@ayo7tfla zrQAfVT6}US4n!kMfyRHyTY}E$X)iN8xe+8JpYzsuh7r?%=V%hj{U9VK2z9Vck+0ny zZoa5@G*KJ zTDtsGyu^OcLb6k&rA;J=#|kzvxca3O8=j;gK`48&%l`;A2{ybVQC}MkER}tLKty7l z=B*rzC|V`1U1jbwq5#VU=xcTDl5TwRe4)b11et0?dCV9s8tI%dd%OXM; zzWNwq)VWZ-xZ*VWvNWC20aUc4lilyc2%NMKb2`7-8odCam5_q2hyW{Mo|i$I-{Ccw zfh6U9A`cPX9aP}EZ+IXw`m&hW7CUHGdL_beLe->U#TPXA7aOp_Thaid{)qU z+_xdUzBRmrz?PYoeR14Hcl1KsUfng9Q%ghD;rkF);^bjoa%D2C!|)1xGx}baFJyv# zx-{<8Ee--!Aa$P2<%co8JJZ|*fvV*9>HD=92GE2I85s;w@cUPU@s(y-{)C|PRUCu- z#sKx#<(}VzD6}Bp^!PKhLn2P>&}-PE;S;xgi+N(|;1ts$IdM=J_o0*91IO9A%I(T+ zE@Z9C-hj{hHBrpy~B6dlEkK?p8c%1VY7Pg z!t__LckVv1Fg`*7`6MT^(B6te7*7BN!SiJ2jHMyf)*I{AurBsjC14s}$LM5Ok1F#CpBat&=PVQ0ydML1^$Ii`akR0a9h*Um_=F8X;~B|O z-rL1974nQ=x#3?5E=->-!*gk~BCO~p<| zXU!Eq0=36G8ieix(zfkN|Fiuu#fra^#9psozrUY}0!Fk(tF;*$H9|m0>gD0#wZU41 zZQbv@ix#ga#N_1gF{R@q0IvX&y2>ZG<@vp4I{A8^J(d*TXEy^@5$L^OH;dXg&S(BU zd(j^qf0;IV-NBF-JYM~N94l+6#f{7M%+jbbZGb{yM2K;|bt~IgYZ(XK3IzmN;;szG zTOdP~v41z9PhCq>GeaVr9$$hZr`Wpp@>E8k>-l9<@$|?|Q}8qGEC1Wos*Gh4pGC2* z+Z~x}6U5O(Rbb0@W8T@$x*BZptk+Sn7l>H^K-k;F+x`j5=J!U(w(GyZ4m7yQiEQ!x z6NwSTXLW!;d=FCj>3^6k29ZR7!$93w{Rv4<2p;RYY8m$M1=BRMkZ|rgAY3kFi(q5x z(V1&qAoBeZOEhuF5TaS?oj6wTUPoRQN>UbPv{&=@!hhEWY|YwP2~-8|JG}7cupv~X z9}VQ-$SE2vNCiUG%K&x9kcJg;=X=CRcd7dQm+_ zrSy?vjlxg#>x18l6JKg-x*$OAw~kusfK4mhn`cZJy{>2KJ;XVssW}X?l*}oincVh{ zb8j&(@66ymK-!stm{;fK=Q#_rO%Tnm|B`{2n3-2~cy+IBdCcd)TvfHrOLc!$Op zWR>>YmyLO;UQF|(#8~NFDKPoqOPfCxt$>NLdHVz5+l=lM0|n+`qqin%Y+E&sxeYVw zh2#bdFPMYC&y0}Z4x&j9E`n1<{t7RITR;Fgs2sISFAM^bGcsmAR1P^wdJ}NFuO|w2 zDZkm3f#_-=g4pzHL-FjLXYtc=?C}Zd(i!PB??PLB_JBU3N)2y1yLtt|bZ25p$L!TM z&qg8eDQ{<_wCIfIyTibL@qu3;rXxd%Mit^ad3cn_QtP&p#5|CahXj=&Nmb3@6JEH>fzG zAI`Py6@7D_JrCHe!Ueo^rTob6NB{0)hOchW*6w^0h_9mL;bnJURwaP4Y0P>Rmtj>& zK*`O^^zvJov;!2sk9%v&6^8;%cNb;a+fR2Nvi@FPcF)vm;uGkr+#df72sExz&<#A$ z6gx7(_17GtJruGQqSKn@&@m>$O-0LmxX2q`7@lYv;ty;BKtWa;BV~|dSiC+<~8h9t?@GJ#rpFkEoc?wp!ZCk zBsWz-04^u6aVxmgn--Ar=NM7BvDv-8CaT4un0vz~byML?{ObP!w247H@=8!bQBpEg z2(|+hFIOGzMke>0iO(!-a6~ijynFT9@u`^fBc;4kz+bL!6(ALR+ms#Rv?{RFkSs1 z%Jjic?yqvL9B>e!05M45HT*RQBp@KKjo|4=MS}3dE%6ESomTkRe!w+Nc{j2fmwDEy zH>vvsuUj0ep0>h>(JC9CKgKco-zyri0RcWXjNOl(z2MwxCgd+bZuNBAcj?q>kx)$D zO&GYhMk_2IUg^0iGAz(PbJ_;dZhVi9FV&Lg9r!UQW*F#2qrl_N>|#XcUR{j z`y+SF1UxP>ws!0`TE339?0b8Ufcy8iF$3TTdC|Mo-yI?sC~MUWkT0I3|I|l*m+0#z zgX+h45pjS`WXV3|E7|Zo`%6j=c^QI{jU6-4VHG5VVfBf&8o54X97;BBHm0cETZw3Q zST_<2F92Q)6^V>)Ap?a*H5nTfL~cb}k5nL^7{RnbgX+u%vPY*@epzk_^SJ{``mU8# z-&Ai9Px1KixQ{J}yhOS!a*@~akJ$>>C((QUH5z$T3P;{A$9RZ($BCd+UFV{##Q4v` zI_MbK<@0R+u-JD6+adCzd3tY#KKn`aQigRjbg$oommgn*{kjZ3`g`{4|5HDIL@^?OiUGm>7gHD8I+A{ zI^l{Z@IP~LZ=(R=$<{zSaUH~{kiFr<2hm{|P#LNC!=gRfzhVh&XZN{5v;sQYUsd>I z#n!{t$5EshX?+^NvM=wr_eNs|pWjPK zLa|y3>>?24i1?xK{wHa6hjhJb&gP^SZP2))o+`Cpsbv?$@nE}xaR=r z=%wOuj+izSaIU|X5Vb^Nev}arGxP006hI*M_T`B# zQmJhZmX!}G1ov5GWf2}TJRp#ewy@A=EA#1rTvpKA*RtF z-Fx%FHuSB>Cr_2GO7{XtAS3$E$?RLdDYJptMt8m+KPJ{0c`Unk*z7$=pkNzE zLS+T)-w)Eun`x+jwSWIXzc|CHIpAfRE))vPtr|RfBj#---NFCf@kgZv0q%B0onQy0 z3{5PUi;a+wB1GQ?!3}X2%7I#$aU*j^Y*C^)nu6WOSjCdOo6WXQ;UhA)0V)T8Slv&3 zbHc!4L?Vw#nkL!6+?P|VJaN8p)SKDvr2woGhsT^u2g*`^j4<`#To1V11-1i+2?4lT zwBYXFJ%Pibu3Sbr1PwhY>~nNDI7wAbo)pVj;#ROgjYK?VEl5D#=f;BEmOFC|&H3NY zJNRDhvp)9;2q4V|2Jvi%0gcD{t>?cO(I*T3-!KP&Se{J?)ZIV)TwGG}MfGz!{;$sF zyqbzTf4{XAlQsE!-am5;{=0~upLctnN68j=HM9P1pDy^o^|je);?4T)s|M4Xdp-moBL3zr?+G`+Dn zS*Yks;eASZIce;#+%4hz(Zm4ha~v_YgW23&&Vf^r6q08N*nq#Xj!_0wKj{Kr+SQ zD__N{Y%b3Ca5^Ur-0-4|3UUWGUj#UY$ef(e?Jn@8-=pX{Y~7o8guOznL1+5z30WCD zK1@$`Y6-q=ZCY$PWedq}TEYw0AWbe)`u;|*n2qJS-Jo1#srW?hY5pC~cwWytJ=nTe zYrwf-XrRyN3%RV`>(aF0QJVFnP>06mZ>B-e~X=m4` zi%E>h{0G!k$B^L^c6Uq%~^Uy!@GzH0Mba<-^}h;I2hdTEgxkyXI7tx%b+{Cd~6+#KitBYxDW>H(dwuZ_wY}bX4`aaZ#b4 zm3-9S?a?fSW31IrUF~1vfjI8e5IRMA3MX7VxDoubyLMlXTO~3sBLfbnHo=#j6vkj? z>xkmAb}A)OhRZsZGdw3DFLkMx?9~QnLPMFEnBtywT%Vm~phdId8zM>-zp~w1|0kF4 z!#d6JDJnU>d#GkaD&6z+C_W4Ev+gcEreOQM#mzShV!#K&R=@msOw0vuEiwq6y)=%? z+)py7G6r@MkKw7RMRHIy##QRjYqn12EIP62(rR?V_`b+zYUst@~( z#c6<6>;pE?Vs5_EsT+su&p?0SfIf%>Y|WZ{_8Kpr?zP?p+J|3tru1d!;lvJ*R)_F9 z-aB23iDz(O;3Y zLl(DM(#M#*pr$4d#N9V{sgq2T1kMwB$Eorb?gco!t4_{(IuiZ+{^j7eHAX6c_S?lEJ}@_ax?h$>!Su!I&=bhQk?@XMmR)$44^( zPeEHXzTHSSrjHW{yGZJQf6URr-y9pQPhY&cr!IM4Dc@5f7N#uvc=OOM2RJ>^iJYHP zr+xbdH?By!{15`!Td8cMCz?F8Dg-o;O69vC#J_%p{uX`Dl{xHNlh1De2q7)v zCFs(NW$o8~!wikjUbMcO3(`i)^?%kyx5sFW)CXeP`2+lh3$dS>-i;#%-TUF3C}#vt6j#!y8RE zkzJojh1Dlz-w<)PJ49wZKKhfLNH;}eqyOwbXXI15*kgeYX?AM1QFGKGbYBl`%ngNPz-qw0EF6P)bp#lxkMpUHlS0uJCCm7~S{7Ol-QPV_eWX(>YQbaYY- zzfES=;kZ~DkjlNGFQHDL-hiOznyV+8E47oBSfUMjALe{40py$`g}R;NMJ7qvnZQ#L z%Rv|$T3LOE#YLSZ-Mjf^kvaA`j<$s;L;vQ4(`P$NVgH$uqejs{&4J7x2w4COqatp?8)RhTW9Y5fhMCTf89ZSrMB?p1pem_ zsP8|q7@cOOviER6FAijLypxrWjq4t_w|)WiJ_9i z=k<3XHZ$abyT1x1$3=Ve5ZTcWYd6rd4?z%WHuo?4)^9fjNKLJ|sDro#C|xVE=c0=k z)jd-Zx14r&#n(T@&Hq#7dU%NcMh90z7fhoOS3StxYhzke?*?b)R`*%!kQxp~V|!8` z4|BK*o)mV#+qkV<4kPgX%GcP_95?2FIJNDe)|&sH(z9bl*K~Gs_jQ~Olr+>-e+{lG z^5kb?;7O{?!HhqNSi&&VJXQY^Va=YJ|A{J+)9HmuI|UAOv-Y{Q!RHT2@RjSQsO7WT z#Uc7H@AteMef}#wR&>G1D$(@==VAV*30fmAZNXDZfiP?6ucMuv@tiv|2)8;(X+M=T zpaYd=o1qf^xn(Z)vcm?{ch^OCJVhUUaWx(pL(?t1pJ&!CbX#VTc^coG<~v{{HD@n; z85u=w$txqSx-FKGBp;P{Ktex`_q03j0;lMbBbspEG{pm*pI0z z2VupZD|5#ZK%{HCgjOhSaH_bThKQG_CVFGJko`Zg)%XhU&TcWyl=d?MYHQvyCDL3P zGe&lbXta$aTQw~t+GfHERr@QdRN3Kr;S1Z(Zg4?V;LFj}RO&M~Al6gof+^!8 zPy*jU1ELp^!O+?5`&riwE{k)&-SE;q%5k0Jn*gRbGf2bjK7ISU#|D{cRpw1QPR_F! zimV|_(`ZS`(vs!K9W9wdVMs)T_u?rsY6!K8RQOP;&zXcqlSKDbN_#(Z2Y@kY(~Ppg zq38V9tTh44ciSwm^Ve%)lM2UKVmZFa9K{Iz0!W*;!&!<`=dk`ccK0j4aQgRrI-whs+Xm0l ziezFQqFX(-2l{U`)Uv_9q6I_L;aDq)QGK$!M)lXUYN|LbHB}ejrHDzC(u-Ny#Hu@5 zdMy8<3d_>!)Mq77Di(rLVK}EuA)1dgSzod}@VxSmt)nsD^{$=d5`^@R&+q-ucknWP z_NUEj$E-=Ucc`!C|B>qF0w?HvE|wMoQ9z74s=`~hd2(yL@Z~yX0>cRUiIcU=Y;d5| z1fyLw79SAwT0VZ9qf?W!n`b?IT;s(4WBI;BjGF2!=)I@t_#4THnkn=VYp8R}O#S7| z%twr~XgNcVgjBN=%0Kqni0To`GvG5u(FXRu>l_THb`Z4USH#;ELpqaQf2}h^uUubu zW&dh%S=OXXWclQE*s)c~06?yNI0JqQ>$U3^1-Qx16FgH%aW9|OBu>*b?fBPZX5Yug zbba8D!=#`^o43?OuX?^^KCMFgXg@S>|qdbvR7{1GQZD zr_Pqa0a7s+sUBRp_XvQ-wbBm{Phur$63_31;H|^++!v^>mE%c!O#}vt^imoOMy{QA zqddhDIQek-b}3a%?%(apnt??|ZSz0tLr+?}x#a)H1sDb-qcNV9Sc|hxy18p0x?EC= z%pUJGkQ`HD|F(@Pu*!Kh$srD26Q7?w@?|3#HIiow=4|!90zFJje4F+^!y?cFrh86x z-Ow>kFjHh98N-LJA=}^`nHv+=^}$m=IUmqqQaBYY$D)XO+TYFSLpw4OV!@aqd&|g^ zrR}Z*%AIG&UALnnH@p7dE}sT}xzenC{lByAL3%79>*`$kgO8Y1k3E2xKd2aKlF|&z zgr^GV{yKVP=Ac>B)^8e6VWQMs^IIlFX08Y4@$H zpBwf8C#yhptfLNJJ9z)PNei6S!Q@eLZ>hqnonS$@haSTP8O4$@8I*OAhZ%V8oE{>z z4|FVjFR7+ioCIA30=P0FQbOP=5; zjeSio3SD;*b8qYyG#FefiQVp}>T^W&C-9XPOtNNXV8rn|VvN@+@b`sv)3@H-YuUTx zVFaSsYZwYgHdeqcD1^0$Mp==t2Ao`B${Q*~o;B?;Tu3GxZAGGr_*eK&Fx7k>#;FM% zouLmgW>Cx0sUR?YexTy6R5FW0bEsP5u=l`mhVq=jA39ffN3FT_5lMw*ds7n0M)8E# zMs29Im-O;Uk6hw)96voZ;qPvHDG%0M17dOHMlpZ-aX@?_F{Ne~$K;;n0|t3vtYv&G z!BVmb_*Cyh55EqfQh&!|20SGW)GNnNZxBTf6W7K9p&s$)ZVVb&f`>UkK-I^dwT1i2 zWNzA~Yxj}c>h7_DJ*Qt^1qqnk#fb&6${j2=3&67;z+*!Q$1pG8e`t!biAd~9MiE{A z^ghVIWkP(98NwJOUSt1+z$t6s=0aJ9I3I0T#G51hJ6{G*mh6FWK`Z+3NX{nQB-hJN z!sq_w4(U(1BEd6uIys&wD+u(506Fw`@dI5dGPMNEM^twJ-}ao zEb@(;$1!6=sC{MlRWuiq8G@-d4OfmWFC2)rgozX=bEjByWHrium&j^RL2t&py?8R( zHCm@(C6VzD;um0ShC;Hv?RX@n11(jB*)<^1Jg> zKu;U+puUN+*2ZR-{H{s9qy?m>KhN-W4q97EyO1HeHtw73{v(fJ99#(ppOjIIpummS%S*VutACJ4g8#pj zxZab%Q*rdaH<$l64g$r+0wVxk(#FRdpy!Ao?ytYXiAgQi5ZhBm$c1yTX2s~VKJENg zBKr0CF++wdu~-IkY0vBg|I5&Ft?ittHtUM{|J{?XR-xBhGUpcMzecXF(~d##cmVrg2Bpe-8W9866>?Pb_Qm?DIRb zj9k9;sXmOKx>?^F3<^kC;jM&gdYwr4xT2p7pBh@?h%}X(>rq!b{yp z=y8zaY;h9b`^(1#u-Zz8Ny(mbN?|JXMo_*ocpRYLf=&YDdrjFtJeAC`Xhg5j7MT`FxQ>@SMq9`{~7zZEZ!)U8Q^XF z;tVOxYmh^@h#p-72?cn~m%B}+9g7SdjVUPrwm5_}Jps3_dHoSEna0d-Oi0%JE?7M)edHOoC441DGs>)>%&<(XizC80c3>)y4{rtt4WR8 z_Z(+)dEg^<*OH(nh|UsW&BtQ1o2#2kT}zkZH-`QVXvgM_z&eW92dCDoPiwJFQ8j=J zE)y-k%4Ipe^gP-I4mh!5jP&7(48!r?XX`|&P}7I;=#%p#@ zNhv9&7#10+ctO!(1e3OI2!;1!f^z!uHfXZ*E_=V&Y1Bajh$NtiPdZnvm>es`rX^5= zvf_n6UJh2HJmSb@9fd8}sA-`>EQ@*B<2gg0((q&}98m7_14sn9_wF5RrKG5MsST?9 z_}6?>U3~7^uErRK;RVssBScy>-mH`8%l)FsL|(qnP2Yy(C!T-I8gkPiAAT6Lk0+6r zlY5kPF<$HVAEFoh2#V2hzf693eUM$Fzj(Gk!5`f7snXyvs`=md$G`+@asJ)Fq2k-( zrIMQcts7-~EPpF0>v~z4Hq_7UU;BJhCA?;-y<*BSV^dpQM0PGj^&*IXwUm#G5>{NU z^^6nxEjLJVJ-Y^Uw%^5$3_KhX1B$@_3?j%075Znqz2#($q9>Q7g(OeRJfQcrROf&K zyl+@>hse6q?nmcc=6RB3EsNls=?1^=M_pAxSo_S!>6FnM%QM)$%c7#9w4YYh;N2$+Ko2jI3VpLFqUU=*7lPWCccrH#wD`c2$9_X&JGLxact^&0(=43g)F#3=Oab%K z*?;+cmcIW#93oh;lQaVKlaP)GRL7BKIrQ&5(O`i#N$Wv`uBYt1*ysW%sGX9*(zzrph^YCvg49r5$SWI4B*`~XhJ!IUix z9nPgog5gy{#l8R))piv{Gy;p9?eZCBJ@jK7-QsUfjLcS5qJjJH;>f|-;|o)M-(^K+ zU59pVwl583v?f~&v*lxYdMwWq8t#zlO3IvkBz&5Z5{mqx$L4}^;FMao#iV`wAwDUY zzE|(IGpqazs>5E&M%g7m8$yus3Q?fqV0VxzZW!%wT)21|FN;`?sN06gTMbjn$9{bsy5o&LnG24NJtEgASs%RHlnu(b$j$4kB88fa5Lq~rfv-yM8dW@aUB4=3 zJd157X0|oP6pF`)ZiJgMd%st+g(h~F{UX;1P6Zs%(R;De%ta8lqNUGR#p~VbgkA_% zd()?yZj~?Ds1#Fs@*ogXmN4p~Riu=<6tB>r>zX|nf(bU_wV38t@Udo}R8>JL$>0eI zHDs4Q0U?#KMWVC#M_94KIc%}{Ju^V;;1xm#@FU%58ab{Eij_ZD4i%cE7{>Kn8_jbe zPosR3ou7T0`t>RJ>8n-8{i02jw1?rOXf;f5bN8~{1l60r^2i;Io_yzFl%dGm62QL_ z+)8y4;cM*0>oD$c#w~<2P~K?;NwXZ-ettaNoK<; zloFkXL|bNyP++Ya0-!pnYE%5LG;{7Bhibxk#_>(nyXuFVTKR`Z@LqCMF@~wD-`O++ z+i~Q^wLR~owg?&|;Q^ldN}utE78I|Orw4|d$KFD*tkQB&G9$G;tBp)gL>^?Sr9xgc zuoD$+A8D@l)|`E}_8%kA1KSs~zo1Iy=lDP>Ada=)>xz=d5NpukEw)~9iMk|n!}HmU z5+f5W^U<2G*K-nN537X_n*J5t82k{&nbJcyuho?2_Ofm zxHiiTc0~>Oxwjwe^OqG-R@1f`BGoD0Q7c^3QZ0!r#bl0)aIkO-!$xFfSGl}~>l^>B z4+L290ByOKeGXwFGyiFre9k`UOoPP+~*nl{e1CP|8lxAMl~$6jg53}Jaw1sUe^$u?Zejgls(bQA-RN8UnT zk@M1PQ+qoX#+O!a8Lq@$E)aBa8r=3n9ju_mYK8lM)G zSS`(IhoiytDzZzLYE2Dz83}o=_`FiVEF1)}#WduMZ8@aP_hQ2< z_8(5ri&u=PoiWFjg#u3`THQx*dYHh8H6o4kfIevkO5cYOM`f3!MgX&k)%2zGwcjYPm> zJZ#BIoecI!+OA7mU1s89+JM>*eSOSf`C-Z-r?`8$Icx}AC z2s~X@sK(4_$f3{u^qHpgc^dMaPar#3DH{(EOT{rdL)?~HdF7_<{T5H9$-Y%;dAp_K zxvDxElkkV_&an;eto$QuK!`JMAjfzcMN}e1GZ)5fM|BEBVugYx^9hmr&;u`d2#~$& zamnQr1?ol!#HdQ<4HVFM8Fqc?+H$IR*jTzkLC@;KZXQh1hxCkIoZXpfUr?J^BMhIt z{rfxAf1Zch{dOmvc?VdvR&BOC!*$AzOa-=*zNaN>GSs-g`AuN^W&Y|dq=;p!xj|o!zG86*U`=4((|Z|0&(S=E+^n7IH(Tz zm?w9U15|O5@LCOuuaA&~Q5|b9L`X@a1`w`h2CgV}#Y*;MQni9qKz0V0-{|X21PF}Y z`gYD<6WViv{+?s$Ntn6nsxmS>i~GKum_bg#Ta?PGU?70jGC~_y1g_At0Kh7hJQm~( z3dPdPU%oahD&$uQuYD9!fe}8JCpWsdFfxUmcR0<;@MI|{4ok@UB~(VLneeFTuyrs^ zq)00^8_FfPNmj5A(!#x)|68@M_TK13IlT1fb$8 zbMg_Y&Zjg0o)|Ik_qEE6X;ZWH=3ulD5yEkfztt%Vy?!dZ75uPmIV}DL_os0!usGl8 zU---LCDeD^vP^Q)7CdxqPqe&=XfZ7_ds6Vc{W*0GXtd|o%|CE;@=s2@`duEK(1qfGoF=$8+d)OEmVk!r zc*6+bCv2hntS(>imm$ktzv4$dPgPUpna&948ji=??A(QKBefAxG$OCVA7QNXN@F1KeeaGlTCTi^n7e?0${jy zJ*^6ORE12qo!~=8qF~_W=!l9z&~uAvR}ng@Vdd&h`|gp4+mWW5laU2XkDGH>va57J zd;>i@Ke#}`l`DN_mSoF)-ev)i{ep;WIPPO^4qVv+ZpByN<{6Y|wW+gb1erMC;U=T^ zCD^J9<+HoWHemtQPIb;$5hg>pfoz&3}6l3cgDc&|M>* z`C{WnOmoxI+?+Hhg;Br+8WtWZrq_voTCIa?99u{v{9bgO>nQ2c&-5vFzdw!%Mxhe7 zANg}}@|gsJ*pvvTSFT9V>4X@$?cyzdxz|5VncbTMgIZblsx#^?UfV18NLE)ot-pCG zzbK3Vf?kK)D}#}` znDz7P+X&a4sd`y3ZL<5qAv;OH-OKm_&6h`l*eycLElU+Qix`dhT<>S|^a z6hT2nJ(fj~hBE67DNff<cyru{|YtqP|SP4MFQ?A{=KCzv(*4iPgh{c%{X& z(r9vw=MgS}?afY;PB&jx&9`!oEbj3cEhJcpbFT!~2sUdKs_2+R%<1Z&KdjK02m+x(t}#Z^2%_ zeJUcp)q1G(l`tj-Y$Hm|u2mlyPyuRuO)+RRZd#Tlpnwp5CB9{Py-^@kA@m0CZ-b$P zF*UVfl(_rTi@p96QpoB^p2U?3lC`1O#D!X}BrQc)J(j6Bf1fdpFN209IccWfUy=?( znM-sCtPB;f{rIMH{%7XoN@}T!afG$kP7L3#Zw<@FJ@XZF);~rWqihOIGcspl)e+a< z6*th1=X#?cSzBe**i;?kU1rK*%bj`Fnlh<(NQRc18-<5H?N)8lA!^s=JqG3X+>|#+8g#AA&c1 zh^KM=kjXATJev>c7UzO(52Qhsem5K^5zyraq5>;5xNWF2z-EOKl8=pTmtR)Xa#>Up zw6mxDIoFG=4Q_@iLYFn!iKP%CRI(X=tXsp0nXk zrc#DO(hPLUvI`8oSw5sOQ;I!eZP=Nx26=xH5bb}i(ZtX38Dt*E^^2yaQ8_BXP*kX_ zFs+k*;yG6lw&jyT)nzkg)Du6_;+r?*m}lbuo|SiRj|p@lsXbB04pN!&|0oUXI(juU zYBb%lDyRHS=lCa#dyvh5{u#t6kZxOOT9~RlzFte|9igljM_%-~$8I=8XKTRY&7eMV zph5^%h>ZKt=S9ASa#m4t#0woo-#8Zo|&8lKmLxttBBFf=J?@+mLkeJA7_x& zZA8r7lQ?2rNXrLmYkoSJysa#7guCN=8k`m6qP*pq60lhpiz4GUGExL6DCJ|F`?` zGtkEG#3JT|49F4uCh|nS8fm~}kvs0jyR)-OeEp2L><5lv2Y!!bJqz0efzOY`rFJEv z%Foi)7(#*y_#WzuA3YdC)+}D2|CwP&)OSZ87T0T276|oY^kee}WBYx4LMT^kO0@uX zbzz%Qvd0XUy_$P}bnrLoMLC7`P-AK>i6Uras0CLO9Q1&VIrJMVUO1iGDpAWCb}DbjnyFYJ~>V5PF>RVswFp)vgH zV2GbpRUBMt_$tBhCYAIjJAn^uZw9X!^kN{f0|B@h56rx$7B_&aal+-cc$X}JKLMi z6z862vBUHb(6iN*l(rDqw@0v#&zpN6S8KRt^@_hHDCdiCAyD~5I00s!@&tCIh2 zXYOT1Yui%8$z5R(HEJ z!05LO-Jozqh3@xIXIuf9YtxD+rq62I>V&5y50@}*kN$j7koj-zs4FGxf#g1+!K%%( zf^oN)%Fhf?M4P=xFG>T7N`zrwnwd)$dr=MGoHl+OK97LZ81n%0Quif>C}woSARj%Z zSb)cGV;-4%1-_RW-fMF&4er*^i(1KIAX7 zs990eR^7SjgyjUFBYSNB-QQfqYy-F0Tt!3B3b=d&KudH*k;lhwsf@xX<60ML?VJoM zK7JkMClueJo346IJGwAkwwSNL6}77hV@k0HbAF+$A?O)5Sw3O-!Qo*9uxB?Iei3q5DVp1;vOd_;+LW7931X}vxCKge z&H2r8Q@wHGF6}?2&o#BQq&I+xbUJz%?_xr&+PFT5zG_zG)CmM867oAp@z=E+qs@6C z6Cg`!&+riP=S{RB%C~rCA(L;@DWJTMHbi^g;^Sr+{T9FO!{`$YJRz;vq4L8U-9ON3 z&x;;g7057l%3ni!GL=%Z7wrDM6Y9}6;}=|PKT}G!a)Msewj<+uK4cn|X>h%c!>kYX zpIku9@DutS25UP@OGwymrlzLCyeq!y>gg}&h$vJbTv7fm7-EuU4N3HI^MFX?snB%( zC*?ZxM$vjLYoJrAwzh~$(;aqIwIl*n8J78@N&h*>-ptxO1sJ}1&H(g+mfhvDizF#S zTvWp?tDgD;*V%wpE6lr=T&hH4=>jhZ_$RIkJu9=A27+3EujT;w0)$*1F$+a=D4GBL zd?g_x!``>i5+Z0t&DMScq_hI}D#%_7q$9pz3>au`tJ|j%N}A6)Qs08Tg!YjKamEqB`@xO z3~^}lsJ{HZ^wzCD#u(|%?B5)TyD&Jyp&vf%jQm|hz3G!QvRzB(f$@$R9M&xjc)lh=((c%3rVCkIj~g~RfZp0e_bfH*6toEdO;W^Xade46q|e!rrH>7VoQhf7Ew z2)0i@Jq!Qqi%m-2OhhVn`TXN5dXq+(SkRY^&B!xKl%VWm4vkzTJ8_XL^}*Uw&}{(S z8VG+~Ff)8EH@VE``h~<8kkpn{{|*GxVHMNK3Vx8pB>l&Gm#Q?YbKg~+KF5g;M2@w2 z%M2La@o4xIn*De*G}|7q@c0mfq6-TiYx{O^EkfecMC|+monuzt(sz(HdmeXw?tD8R z^_Xd>Mq;xt?)_Wg$~@9w4#qgAfW;dZCV9Jtmdy7iG3ADOV`;i+7DBl};_=B zdNJ;eAs#l72)GcN{fo(z&k_Q^KIq%PR--B0r&?$oDJQ%HIktM|nR(?0e=@HWyQXQ| z3?Kmj1H=>`QLk-lzuB$1`vWnszPUe~GGk7!T)ISR92M%}Q%r>xuR~0wN%$Rz;@E?U zbhH&=bj-ZsO2cy)4xWIsmt13Y!@hQ$Z93_gM<6AR~`Bw*Gd0lBAYfAAjAgluJUYlfv#I zyUBgXDV@0P?y{ubwfEXUZ?)Ab+hm+w6pm1=l=UTx(dW0O(x3>d@ZqPsOWnkDYM&G7 z3*C}@e%#@GS$#{E_wGKOz4K#}Bwp*F5DRPXSEjdLoG^8DqwJY~nnY%I%)C4; zCw=xFaHV^EV!&YW##6Vip<*hik1N5Vajs-}PS$hyFGRESaQYECSfsz=K<=F|t7UyF^L;8Y-vu`R5W$$ABaSUj2xGCXWI zA?pxKW3F8{H02>B@w6&KntZ*78ib!YJO*xv>1e?GzU$ladLh4=?XX@LDVd>k<`s>C z0U4=|Y9@OtS$FKbIfilpq+ey8(1|+nErVJi$=|Q(Uo@nu`xptfx^6D_#2u8KbNgA1bV8nNl-K7cm8pkqIHAbEVlj zs*VT~KUX&tPO_FAfL?CA5}}VGPhed&jniTWXVi<0$E^QS-JjzyR9znWWeh9Z{#zrDcR%_fzl{d;#GL|8O8R=ooL)40~2No_>_XmgnRmN^{eYuAxZ<||?mAQxi zWwt|OJqbiLUw;rKee$p7Wpa`(Gd1gb8rWa7Tgrc821}`uYUnRjB#bE#PD0ynm`tTh{QxZ-eW30Lu-#T%<6TydRuN>+4 zc7dim*S21SQ$+SaC%Hzugp+Q6q=vb%#q`kHSEaYsWAl|*+El993KB9Wt&8m2k*EtL%lPn&I{XLl?yG`)4?E9( z*m6Q#10^M@Ca#q)#HvQ$woao;qXSZno^ zy`!H0ehOl!dr#1ESeoOh9$}}>Y5w{88$&K5nfI}?TU2II@bKFe6~i|#hUbRUd#uV5 zJqB+@$Y;xx`4hzJXD-v~S&#dT$%gZ@@EPx*MD6^9$)z@$;&q^5O+%>f&axl}dO9MN z;#cOhsoJ-N{GRvP`#Yj^1{z}^|IJK?d1)<%pKyZTrq3Y6xLuQ4h6-TWKo0_=5*T?& zaT(NavCT>~0JztG797|J*95i(DeUggw5pvhm#LyP@NgA%b+x3K@@+=dT9DhjA2MyBGfUhbZQ)&L8JtyAwf>?pjGr)j0JQ9w z{RjQG8CEDgT065gQ9WoAeqa1C3i?0J_g_Ob4#UDDu~t=zT^Adp-@ zD}A!>y?a@jd(Fi$OZY|idaLc98Tx)0<(~azXp^UVDyul){%XravFWh7y1L&sPIf%H4%1AJzNUYR&5B}`JD-rJ)5wwO^49l%mtMN-Br^)I`?KB z=ewb$YYiZaH}Sp9h;Q2TMnk-8Z_){8%=gzi1UV+GVaYQDPyXRpT3B+^R_zMH+tDL;AS#Z!WsMhtr_{7M}|xTxEnLE z6pyE(NjYP`^oJ)ueg9sJ*jh4?LNfL)`6(_;;_i!+>EOw=f$9#Lbe~O`DVyGW_L5)kCOifUs;>^E6qPk(aPL98wMq4aLwAKQ`6oWsM*aSQ+!oz;Ch*RF0c33q&p3(CFyszK-Azl6oJy65u9F2B1t` za@Db@9OL0Dz=jH;{EZEi|IY!rp(|<=R%O}3-3cffxlSZ=1F4f`ZhE%z&*s*WC;(uh zCD#U!x=DlI=lDs#hKxv^#LKsE%W28p2@A@@rMoT; zWaczN)w&n}9tP(ly+aY40$bDWS#RWH>iHH!EYbo$4kb0A=X*Ky9fq@xeqO_S8f3|- zYe9&qT?Gzsf#{;Yf&rc5yvn5a@JGnHpBtlF<3@4W?nVUTFOLJ(6*j3G6L zH$b+IyD0eFyUhkF>b?jYf&#OIA@QyGC}lfjZNZLa_-S};@2e~hN_+VB=`X$REeF!X zoi?i56bwksOww!LPCI%I+LuyGPS?JHc?Vt6F&*(_Js8JCD=)S@8QPp}cge>0kczzz zHMg0c9O^0wf2~0u}`fPuxDHH>QrZeF0L{@uv$J+WSD!EI5+fW>qPfRRpEr{nV z$F$CT$s;o#Dwg!Y$RYnRNPZav()AyEzeGyf{~55=7W7j^zM1!MqUsuul*^zu)T~bY z1piu#e#I_Re$n5hfDxvYj z`R|xiO`<3A@snDKOT@YjyEz>ExgEISwPC@V#^bRXa{cGbwvWgh_Z-WVJ>7C~iM))N z#@?5`tqmD)2#tU0G}0gJZZY*!^dqBSk}hk+u-1I)nU|1bpHfI;;@rK*G;PE?Oc&Jv z7rxZuvgbt;rVbi*PzWYJ1&ffJ2t54!by=Vr6`GiwN}r9*&&_-x8UEG3;io-VkP7qt zs`~80oXHEi!B`vQdI)b*9980+t5+&5b4D4}DwRR$O06Tjkvet$ug|Ai5dh}c+5=g#f&e%Kzg-3D*ySTZdwk-BQm`A zF6v{tFo)B^ZD)8ET@Di?U3?)uc<*<9TC5aOm=KE48geP%a>lEehiukPL*|{dx zC94LZfpHiyif=X{ovd7J;L;W^TO8gjc8OQ28eD_5->M8bpoV3|6KP|JgYqfhBgq`S zXd#1q?4x6F*$usX0smTnNj{YOUt~hE@D7z4zWP?y& z8H5Zc!42K>4GEY0(g@YG48qfIx@4nx+$9fvzb4Ts5&2mc(*&x&^%gZA@tDaw&|$X! znn+#Zl2hOvBq@$gOw->b%S~g!-f0)IPI9^I+V#|zN=d#suTGNqpGSJQ9;{~xJ0c+U zPF8HXGyCk%0b|>wOdTEa9OAi$TwK# z4#%v?87A6ukzDzHxf&G~Rr}PlxTHrF`z3YlT?}0s1O3r-XnPQsJj~G=bj68!GXqX1 z6i%y?$mT7f2>>?9Kpd;(?y8E9mS~^imI@GV4||;6I?f?L^tEACU z3W*twT_!N~j#Vl84cA5TUa=Ngf{#3?OL0Z2K$4{lHW{_RaUH|arV3L?+TT31w z5YY$H?i)R5y_?K)akdUD>J?ApoMVXIaNteNp)4bv54UE+OVs)o{8;aq0^~U7IS!Lc zG@EF$h!+r;!L~LyY&;vvuG9BvnJ4{EK*P`e%&7{JY?b&uhmnCnksQ!BzYK`_F@f9p z($B$xINa0ptw^!b(0m!(q{Z8RUwd=HXQ3jsBkDY*V#tR)Q-7 zpI^rHk~&^~wm+tH%?$u0nkzK_is@9yHWb9yeDuml1PD+Uy-Sk0ep_L%F5E%wMj-BBuy<1cAC(tm zt7^6M<9g%gRbp0$Hns}g^KMIE1u`)#E3Ij;lbgkdXRz&&+e}#nNbc=AN0Zy#^$=b5 z(B}CbawP1Z ziP*!H>;pAG#N+iE4nvRJ0ro?{lt@xwDphpaK@$2o^Wjg}b-MSkmefD&@R6{UI{h=$ ze@XG%q=$d9BGrCvHw(yl{2^c8*6Ydi`MYO_^|i)4048P&VlU$^n*CBIF8N~=MlKWh zu{f>J%jzT16yc3v0B)*PC?7oIvj-dsjY82$7fe~vx&-fI-=-X3YL~^~r;_6OP4SNM zsz=1%w2MDB{Fo_t;3x%H*+ij*6J^}H%p?f*Cd*?zFAm|!Pw~yXj2KCJB ze!HqZoJ_cCuZQ3~TKbRQ81nWFvVdGs{NCgEWG_#k$I;8l0u&EfyTLMtL%`apGeG$6 zu=mwM&~kRK&MzQee`JE_J7QU@A;<)x;?{-uN^Q!a(S0{RT(-7{p1;qSnfEVaC2~79l9YtczL7Zu7I+JKMt6+hm;KSh-n76e}b!NR8SN7X3Gy_i~pn?LCSTqtc zT~7=QJPN0)b1zCmW`3h{1Q|C6dcl7dHu>pomL`p6-`@(g}8@>?i>T~;!BSPv>{*XB{;*V%pm5Gpb)Fd8uHOHu%ZsW-j0?f z_ha9Ju?Yy3=sSl1m_P`tv680(07fTlFSCUNMv)T@qldYgg0fyBZ~xjQ@t=IluG3Z3 z#He@=YMP(b~$AJ#hUKvEuCvZLyreIwocAFBSUPlg%nvC zX4_vy=sD5p4X4G_54?g@i%^WB%YBOPTAqPTUZhjHJneTvcy$7Iq55Z+{wB!8L`~Zd z6ZVeTy?y5m=9QCsE>onVP633=0LL3KmGqF$_-3;t_gWh2dpC7Z>?m_Jpe3SlGP`t3 zUk$Vneilmm?~B)Ac9frf^&vs3D(v=tLC_wX?!Y$9opWq1xhkO<1rKfP_59?6h^|{P zmQMqEgwrw!xu~WdtbbFbi{pY5O9Ib=ag_p27FuRC6(xs&v&;P~!_sxUoD0hS?_eUX z&Y6994LIYZ7JKDIJRow~xCWPo7|#=qRDyLSPo+O5dl#NL9xMehw>_*x9Qtn|)PlTL zKoxmn#7RfJbR43@TK5CqRV`!bRW3(wwE7=^$L3cr_(^4HTYwEp#hO2i+838hP~`^2 zteAIb68qQ6CCxbU3o=h&d)GIKHLzTDpEC^_zWG_uu|37mX_fq;EomDByN(ef>LzZ+ zakZ87l;u>7^ij&=!w&!A|CipIf>WFTOPM5NfP{*DZ7o~-{d+IS4-kc@^bf2UF1DP8 zCZB=wHB)J=F?;Njl~V=}xy!8EB)qn}Knt6DSrP{5ar=5aIW;H$okdrNMAFG6E(-={ z;;zmt(I(_`E7hNb$xawV8B(zRb5BCE^J2W$75m9iyQ&Xe?~Kf@Z^wlGRTGiaGA{vE zSf6f(=zIktM2|z~pY} z41l*7^z~z)4ANH9)YN+vrca~w;Vc=8HrTdLtM&!VqQ?fLH0oE;3(FvXoLw;fs#7OM z1AB6=*0oB+opsn7?)7cMynMV%P4q2-yZKE>M+=R{bET zDw3^(a9@4h{;2+d;iI{OD8i+IA#fn=H z4SeS8O={-&02eZp>!YY1#ba}kRpn}GYJ#h##gy}@Vi#ch51Xjz7<_t;kXM>4+e~}) z`khF2>6cc;XYVqD$y|=~pSmSSb6{SrQ)z!9i*?f9%Qnm%*#dkLIdtG%H8L{I0+!r% zAL}PBBH6Ppma2~$a*mX2p!jx7cH&GbGjVFGvGHFVTzy#R@>*>T#553BJou^c#VR3I zUc;$YYDdtwr*!y9pXY_kCY^w>xdY}J#bXTOU}#edX{^RK$3lsEpfyXe)f?TU`48G)WI2F|XiU}*i* z2C{XAP7!?XolB{G`wXuA&tPVC2s0e8T;+`Ypj-;pUI45nLzQIi^$pIq&r15b~ zEj^swZ3v>0={8qm<`?}-IZ?xjvYJG}e1`uw9j=it1Q9g9oaX%)?vl#+6~wk4LWq1? z^Ce4>y(>QN*$=0B1Sz>3;m$m;R~IE_!bK`R{xl{=2?P9WKp4gH5W;ezO!idizN{;Nfkna1%tu*-aIm3Fx{DRBOmU56_!Ai$j@wlB}RjjU5daFLn$tZ^k8z}xD zUhgEq99qS6etbpvMem8&GtE^eE32xZXksX_r!rFv-$hF;yM+Gf)OG|!SO8{Qo-wkO zk9ab~#6XlRJfx1`FDhQy@XFL>Hx#l<{PVK1h`&TvRdpz^C3D12xpa>}!Fu9L|lEv%%hHqKP8^lw0DG0h{06` zsM?KD({j@%CN4gl>X@DgLj|@1?GwF<@%Oc*HJ(BX&)&u);GU+%4#9=UJb6|ZEQUU~ z45%!L{9hKp6jU*XSIhefO;8bQzVfeot*69va>ydJo%@xK0PRDt3<;C${ZMzdsUlkq zyCR4T!{k=%@9%u?;%@U|CHizJ#I2Bbw$R!^cIZouvT%?UzStWM5|#lhNm(I_`5Y71 zFdwM!BAUyJBgTti>tB-?gL#U2wV=yTa`9rwi!C1pV5;JNgMW71YF_xp|0;a2@FV*RwtKbpZt3a_4Cw~(>JJe`%-Ye=HB~6< zC(3mSHdhTg7f6M&9#?n06jEn#ecQAB=d0K}QNP;0I|x+C3lrZ=>Jfv9DsTXc)0o)+ z*{^|qEa$ax1#Kc9L6YTz8VFuQ74t7ul;qhuuY$24w`v6<tVk|CB zd}{xtv=6|=`VJo=6(oTtURqaH-2WWNS5!X;DJix;iw$|*?5af!^?PO;;dW+lSy zzg=*kE)Xkn)K&eJ3J);u-EOi(ixhCkRR06 zgVqa0f*sc>7>rN zlaikbd6L{$)l%q+l08xyCpy#l+auP zzXw*8=|L(g1`ktVCreqgeD|w- zdq+lgr>a{IWI~!acLbp~|D?s2+?9nAZ4B`_62wHqk z0BCz$)8U~dY4U5}fg-Z~mGtVhTmA0nXMqL>C$P$;!w8P)A30u**8y z+EDaD6+L+NslUmLj!IHOoZS%o!SN5_@@fy3Uz`8Gg3KXk+=?4vGZ@{N2%9cy99`_d zc?ndTMi&{8d|FdLDvIs62by!j<82A_G=S+f?{gECIplkeA$5Wj0{R8ZpoIU?iaWQQUlu~A zz7RcEP5t4-Q&j|YKXI9Z9o(7;BpTGH>Z;byN2xc^{cc~!8E-a8|2 zA9itQ%AGQe&4*7~IDdgDi_v%ss%jIW zCGqS9oUy)SB2K8=j&+BxO8ulAp-EPHq`)5jI1<Kz^!6sG`&V32n20*f)qXaQ?Y8SrZ|KD5;+pxrtKfJlz#vZ0Y^Y=D)IAJljxBCwKNnKcp zW?Z~dSUOoGk&UAH8Kb)@KrUyOQ;$Q{eqq^xC+oY>AX<6LnA>(g$Y=R5R0-pG(H`#9 zkLhh3i9gX1?>>eEr#+R17qlt-p*8+>`k@o2_0X{ChTKB62sejQX==T zJU#m=*v(LFCqtaTk-0&i#aKZ(RwSE9XbZ-|p$KsjPK$Y6L01hI(b${w+*TuNh!A&` zaGmpw;4ZGVzYD}t*)>M`8tx^Elg0GYGneYk52(z?a0I{DrRBxPX91J9nsfYm~HT zidJM!r(^{Ax~nlS%fQy*y|@O~Jr@~rA2~nS5!AI1ne8E~H%a@CIvwhRBk zP&p=)_Q}jY1Fs8R39wUx=S$3t6xZuo$MS24k}~YZ2}$TR=~0p~Ij8iyX|J(1^UQRi z!D(rerZh7M^?l6mk((PK9wFsAp-;V1Lz7;f4#DTAOz7{E-yI{XC7ojnLOIBV4sPk&0mw@Lmde`{TDT9;IZNS>fYbhTKE1`Ilt zBHqt9sF#Rj4{blJpmSpOtaU0~)ga0847&O#DGtCNS5xW#oMazNvZ+e+y&H)cAFoC% z1#L?OD310=mRhDMQ@-YPQjKZ#KB>+82h(f^g$aUX?k#J#jJR z#;~>nul~@6qrY2P=xM66qPfR1Xavf|Mjf?EDpYq5RvuqCZ1h&~e|tg%4!vIn#oY8I zh)s#SW^#*(zh4>O^c1oq%#13ou$%Qw@tk|;mdqs1FXXz@kC**6GI7ZfirT@V4OD`W z%=%l@uzeD9SxxTHVRE@#ewr4Bi}^mlZKi)nzQl#|FElQN;=?E>e0Hy9LP5U)Y>E>sei~=TagL@X}#L@*gptvaI7o$5@$IFVs z$#zfw%JmfA$S%_3;AHkX+T&b-zPzLl4lMZqBEoCfVp^cu(0y)S4ZN8~J4w!yxtk^)EfBQhB&8%o#K^Z& z4w9$bRn)}R>~z5;kOxh(Cg_Tbi@Rm9yX3N4(klMeiGSoszZX3C^a`mDQ9`Ykx@w66 zl;1KVtsP2m1)^K;`J2xzS-vvaqQn)GTPFe26#)Vn+M8W;;t>M4Liugl zqYgE7=%rWS2t$nGx!s%wQ?h?^sX1y-gGvsY^SExfmJG_CSOtb#d0AD_i_R_Hc0}aN zUQJ(~@5Me)XT$meg5#o>+RU@q`Nv6WA7tDgfVcV9zw<;+<8NRw&f)M^Pe@6-=ryo- z-+tCYhdStmE@wK2U0%o>Ln5S=)Sei2sC-NCwYGP_BEU^=H4id5g#;FDXJq;y1r2l>LA_Q64)=NJUyHa4! z^(Xw%u1?GJ{jabyK=mnmUr4g^31PNXEOaQk7gSdnwiB28Fd=)b#20cN_y1`6>Zqu` z_v@iia%hkoIwV9&8V03%C@Dcgx{*eyp}QNThHmLbkOl#1B&0h=`n{j;Z@p{HpR?B7 zx%ZxPo@ejk&x&F;}Nh+ z*V>zR0*V{IHGo+hFar~j_bWgZH9*S^MF&jV2Iayx-Cg8ilnOI~K3G^!R{>Y4)rS!s z=e;IRwgFc{#@@+LRh^Nh1wQ_AOy`-6QA+`VXeV=SKNEQAwfh{4r)QIU4|znmLeYV8hhUsI(dz{o9@ShtOH)$PWCh);5m%A zN+C_y-H+y*Zs#$a)z+X`3~ff8<__;@2LSdmNoB*dXAvB|_}}qb#1ypu6meFYbZMyE z83^x`IV`^GFnUqs@3fe^Ag;;ucFBN^`!$CPy7NcT+fGlUWrI>jK-lL~S*mS6>tJR( zlGvXjwq@OM^EtkiFn7PY*Y!Z6Hq3}SjBC(i1NS}jGkC%1Fil?NLP`4WIG(eJ9e@rhlB5RFtD0RZN97sJt+ht#S(2onnKNcvS*}9x$|(Ul@3`zz=heo$J9gk0 z_yDw!6C!JEo@!9ze?OIgJW3P)y&`~B=dxdtxIdLunYqkNhT92pLRx{szD?5miovtX z`3^s@uaFT0#Onb$N-rRgLd_)kx$+pzdiI|!0(ryyaYo-=0Fj#i!<)cJaso2;ki?#H zo-R}=ilQVB!SY6liyX%Zk+8__4$kB$1W}ZiCr94oRM@-XLP!=V>b4dBb$t%i)LDSg@xWA89=_cVjrC`}()bq~Vfqhr#+ox*`h8K2q_9k<7JgRE8;>#r^aFekb_nUYblg*BJesH%NJBsI7--F z+<=uO@&i;+W{}#EO%;uswCPmq6&0dkJj0D)6?4fel0jAsa>I!3{%b)cGua%V* zq57TNy96c!+Um5km9>WzgvH8yu}Z)b`IFl4eLR65!Lurfd-?I3j|(CTf6i)U=gX^X zx~hTky!!MualqEh(qm`55^xeYmVuqn&=ODCG@{o0Uc%tboY)wT9BNMo$MYin==!A< zqSK%8MIUkpxh6R2OTN)Z{a5mII1ik;$(*TjTjq3kXpsNX%0__~&V9he-Olpl^G1d~ z;In;wnmp3~8GvghMi1!=GKEaAY9#bqeb%;jouTNuf$Q{v;ZjakAcd&(KjCm9_P~9? zTp_y8Yd-%oN*J_k6qAq;oCH87`0kl)EGCxxn6dwj1%IEiP~&}B?oBhCt%R)mg-{jJ z@8Q3hv#iehX{AxV8HHZcztz4s%$A+Er?MLE!rrI6SP4H8o#@hL(!q9yZgSGNryz$f zGq1pOPHM=r#8O>Th&=Cgs6kVu{iX?B3kUK?rs~acpV8{!k~1d;bzHzP?{EwMPcPqt zAA(=c*PFW-q2B*Nm^)`pQS72(@oaGECRBZB?IO-HZU>(yLdDsj1@^~ zFC&Osa8jpj`4Z57LWga}$~4LW-C_L#7lN0uYvK875PQQ| zIa_4MV`qI@R!yuf=$9-G|8&J(YEf?p_D3M#4^T9Xj~-mT8W~m0ZjQXEIC2}~0{lf+ zGQ}^eByWi&1+b{rd~Z|z_sAa~*Bmo0C%2VMZqIdX_AKpZL~ymAt5&Ef*c>IO?+IyO zu|N=lxdKI35@_ebXaGJST(H zBBMn&ZJMAiES1aZ*8a5gT7_M)aMc^z3qyp(zt8~0U(W6dS$$iN#vM`j_21bw3_akh zJT+zld*VUwCqlw%zQt=JJPEs~7L8Al(gCCC7QIEiT1@w-H?rwm!v5r_E7zZ_ISG!kI=@)Ik}B)|1`mVb$t11OQOtZ?NMwbruxx6q+}FfinU zYan}GDm#({)wyTEj{G8v`7CcgMU$flM+P@~`wQ4IMJqz_&w}!(JX83Vk7uk*GItS) zb&9-kb!wvkWWQ~(q3n~TrGVl9qdB%|t)5-0>f5{RaOqe73TD>hB|fD5-UwD9J(yBY zjO)|fkOCLb`nDL3?_6AY(~Mcl8@_$Ne`oD8^%$h??&4C{*7hP9L#qq^(2pZJ&Kohw zSlbq@oHkd3DRN|ZNRh47VeJatg{+{9-Hg&ool!vl~?RfD?2G=yRjyU`z zB{O^>Fy3V2m@j1G^P{4G8yY zi4Dbb#x|!@zJcTR(}I*h1eT#m{m%k6zcsiE&9b%VUMaKnX|9 z;nBV{QHF$I=jzurY9PUG`80|^z`tOWBhjuTeQ=de@9}$E3xQ3d-3PdlEhSdUAC#Nl zT4uo?t_}RW-@IDnQhCY14EPO&%v45d&=AIln)b5ifa7skJ-fdnz~KZgoyUXSr6FJMmP#|c)`cn>Vr#-T6q;Y{Q1Wylg?`xyUJ&L55yr!Ul`ZH4?D+vy zBwuIbsKQ2}g*0D9SCdi2nBsL&=n#wmYYBIU2`|t9_Zx#W?TMqqH z&$!86jE5jB|LEe+U9nms-!QvZWRuxFA=C}?FMVr3Z&TyV!?R^ADmrmJaKKvA(%oJ5 zRe$Fr{hJY!8>%O9K&{sHkyO>;x=WBqoL1<29RIX4 zKea#KeD-(eP40_eUmDQhkiE=fR!QbU0A)RWSOfe-fiM~8o$>6X>Hky}z#t&+g-s*i zQbBxjZ+pUJ_-XhV?-vnfaG4^hluU>FPDrZp5rZy|F)-2Da1oN$`D7ljE}1pJgWSdSg>` ze|u_rW|o#>`Rwqq9X>q~JrfBjPc2(D0#U3w-Jp!ekJD0L(W>Hbji53N7ZbSo_c?^= z8A$|t>2~k<{l9<0+gTay$D$6FnTl$LKJ%-zRKMoCPHh7=s~?kjy1Z91L(|`Dt7+ci zZKpeFHYH__dC$HoOUd?V;tm?}`e+wiAHawg3V4RTd>DM&$o>6qR=KyY3(JH;p&-Fv z<5S#De%Ze}yG=@u5%fIaw5gY=^`S3{ZAu&8L~lR{S#^Y!SmIK+nTxTQdOIJ}$OdLw zPkb$1XdP4KZ3NxLLQt*ILBFD~XS203jt-e-+}w7V8+540(FubU9=wE z!EO8&o&;A3M<2~T=51&fCmnk{E#w^JDr63>$$Hyr2WLg~r-!1+6?|O0&JUx76acwn z7KA-)76AcITU$&BXXmF^{)&obwiZMRDePWhcNo5G&$(VsSG)V5@Y@$TG|pvKcddF} zwd~|5v7^krE}L78S|QEbKL1@&HScO>a(Eay33yIa*w?p}lkxB0jsLLOSk=e%W9riU z7%*z2g{jOGg^-iU!pn+{iAi$?41hpDGr(70bqNaY$;t=?0g*ZT$H!IaM(s}0P})|P zg%y*#pGwD%m!i2&e}Mk8(dF}w`nsCL4O7K`VZR2gCwHAEXuf`*t2y2M+C7k0XBjUf zP~0DG&u7DcQd#o;55Z2>({PDI<9-|PUqccBITyGWf(pYD!F(j_HPMB_cGBV25{w}x z-+iBPoA~~bgS5EpA+{RYy51=quXf5l!_|w1ia4ya%0V?Mc~pMkH-q2_ZJwE(N$2My z)pG_XG})k`qtVg-Op;&%av`0Mw|Jfas*mW=WmU5D-;5S=89(BR9GaSnhl`zUZN_UI zC^DQc+UI!vT~cBPN|)nD&aQJ#ki+u#d_;%W%S@q$}<(zI=L*L2g7jk^L2dF^rQ@Nm#| zYYV+>S$i}b7s)xFn$oCD9I32`t?}X^^Yg?c;7=T|Gbp*3(2Po%WErOv8QT!pBtUnWmv9U z-QAJr1U%j(O&7)*a~6r6^%H{t&;uWzyYt_M(sAeSl@~5`aloI77z&fPWbk|%wemND zekXZF8k+7U)#8|9?TWN$R)7Eklq~vQU?9T}wqhp;f)sLu>j{@o@?)h@I0F%d^dJSO zv^MJ}N6(4TnB#0{!eX`!AMY=$?<0SL(dqUZX?VYi*ok2+P*FFX$Cj-Aonc)H2(u#r z6N3{1cQ*zi*^5@PWuQp>Ser@+d!9~Q7_}h~wDCNnKU>rj`q9ym)oNjBiJ>4n8zb_wN-W)y!vPSM&_8kty}UtR}(prD6o zF*CXdIj6O>WKfF0hlcX$JqUZUu@!q(8L(W5q=cD4`&*>)Lo%r0&sYOb}kI-`O?- zuGJPrB)*_u2u15Olu^t2y^vm66@R8EBCtSo8UTowCk{}sz3SFhi4j4vi;GPkE1O9K zN{Q6AOjz+lepXKAsR(t7br@8B&=W8-r!Q!t>6i>TUhPnViej~ma)qN$H&u$#`tL@( z2vOy(Ptqidf36F93k)|OtDlTuc4xOChb$Twm0N8<%byz;LD{S9!rkhFGW$cLZEWVAu8Dv05b_!yet(_qLq9VSkl#G*#BDr((CF3SbVKf_Qdnod@h@0m!zsv8eZ+jxBd9eSMuLT}Ndlqlb)LNJaoT z-GhD%u$o`tG3qQu0DTIm{TlRjj5sbq8YTGMNMKQq1rc%K%#AEacSb!A@#mL7?nGv> z*KU5;viK?q@M}FX<+HDfF2_CFg8&Q2HVHNrotGjz6Ayz{X83)hVgHF4v-D=~99EDW z-$LQE{#!Qe%nG=t?!TJ3An0+zX-3k(EYDf2`eaOiTIyRQ3d5xiZ@98+(?7<+NWMap z45_4^H``qD7dq-cu9;4I*f%l<=idUR8XN#$)_PAf|7TPXFF|SKweYlpiw0>Mj z-JHGxNTSEHMPtRUfB%qtJnKAp8eh8sh|*P{!GECnL+nBuJSt;Ii>dK!58*F{}vls03BBzPLmy}G;Fv9J_C z*`52Q(|WQL(7SEwiLUiIi{7)cG;YkkaGUd< z{E{_)vD(AKL$R8@_LJIBRZM6)yk{S}zPbX^EcauuD#ZQN;gL{reYLg4HbKTzcAYJ+ zGb@r`70Ywo10crb@rLyRqYJ@6V~P^_@em@X@r1mqkhDBo1Qm%J4wr=RB}x+ z&kA=|?~rsjg`Ku$;d znt#x1k|>K90AV}ZWh4*>=Hpm@ge`q@m-+pC*|QKR>N<ZR+ulslID2$RNIlY-r%4P>kcUj~7M7yu&64biVuj`F)JN+Ce`2N^`XRG4m&?~%@ z*g32!`nM$rM$1J+s4GS@p&AB{+9=}Ux*x<7KU3OLs5UoXN&h0hkv_M0_tdwV`BvY< z$l|NdiR*aBNhi7MTT8ZNb}Il7W>>0X9%*7?9KmE*_-<`pZWojZ_!N8(M2*OAt0^ZV zDKAee<8GzOz+B|c>#@}cdBHzCSogB#jOl$n)|g^6WjbX%-@A6t6LG7P70K%kz-yFQ zOOBLc_qUP~C*x|QXx4{Z*Uk^``3JF_g{ehiS?NLWZ>|oVYLFr%#eMaPkwGvo z*4ZHj@d}4KIFEyBR)oAA?SsNs1+8Q_bqyTuDFQBPjhIuRt~~4((M>7(^v+dLZU{Aj z?ep{J8$JB5D{dLkVYWj3JBVNMa3j(BdC@*%NCuw8)MkdDq9nx$D67x)&TZ}ihRzm`_bnV9lBN+C3{q$*t_C?>GW>EsV*4H4>J6{LAH1cZY9U|i zG_@@J^*6({CnCj;I~Lj4B)ICiI6YKqgPp^Sq8}dgCieLZs#{{p5AGjDtEZm$xqA8C>`5P%JmS|`k;a5|8aOOnp#Oxe#Uw|#!l ze_UqB$t@(IKPu_7Gb(GasJ&F$OSzxi>Ms&iUU;twV;r`pqH%| zu9BogbyiurD9`O~DQ+8U4~rF~Xq`TB;5SWdV0)e!JNRdKR8Au;ch4+%90RJs0Cvs% zU~b-bXPFlor)x-XfT%jKD<<#83oCl=qG>toazg$4dyK)XySRve`gR=`>3~&>YZaF# zh<*Y`D~#&otApkenaNVtv-gzQu9=mtED)$gN?e-;P4>ILZ+|3YaOA#~9YNTShIo`C zS_)YT3T<$pJ1Hu;WwUbyXZE3RY$u`MLoMg$puTMT9m6QRAE?c!8e_C*OmFuhK4nN$ znmZQM{NPO;uN+n2WHyNLNsf`qBmTKB0l|Vt>+TQIW<68=E%AdxgGv;6Xy4Z6ibiTB zC-JE1=rt!avG~tjGREX0#9{vNU+S{iyvNZ=(Vpvi!e614l9iin+jG0NCR^b1B(9<| z)Uo*sHL!|X4unnJT_drvF_3s%oXSJ}(-f5VJgNr&^ic5rCWy0n^G6P%gP3>E0gs5&tpD$WY%L1VD4TDe#lygC_&~{gE-C7$G-0;`p#h5R+&2v6v9$xr+?CBEq3x zCXL?~`*EW$jN50AndePSO>I+qtMC1n>gBGw2Cf9*vLDODEP-fYvC0$UzqWV7f6&qn zG}N55>l*vbK3-fil>DFzxbO5i8EkzURyfuu&1V`O4${yJYS&K@EW0Sh^DagtZL{56 z*SvXZ59z}pwrG4fJkwf&L3XNq#tw{Kraman*8iaOU+=t|n^OyW?4Ps#f%mtG1Ld8M z%h59s$RbF|#2;o>p(*CS(SM8ojI8Lgpje^0`unW~&l?x#oSnjCE##`}{BRD4v`A4#h=0YD05@KRv z0G5!GCx$asMVnfAdmDgbXhg}(UpG0w*u{jw=^5CcM`38RisZ>~1U@Pg4lFl2|Irta z60#?Z*V>;8a{m5J)HK(9&@yFQz|duwd1hvYKbB|bm234pS|D&0#Yv%k z`3P}}CO4b`^dGWh!$O7x2X9{<{^gMk4# zSR-!=61w#=bs1FAS_M-{5mQ!Ei)7J)tu8KpCHq$+xV!-5>E%tnEW#|bdZ13~#`@DA zFZYc)8alnWnBn@}_qM^6+u5E?j2~+!+}g%+Q6nuY2U4)NM+r^K&h~)5uq}5tqI=_p zEk%~9x;<{>)Pqq=3kJV4G8PNZLr|69j*Cj+~Jm}3G5vX8`^s}H>*o;ut7A-CQWMkm)lR!OsC zI7e4%qsU&|P+ME{a}`UPGWspzIoIfDhW4zL!)q$>$A6pfI?t0%F#U6qK_~xetv$0 zx3|Txj|s_Lwkx^OXL(qz@A+`jG&CR&L2l&B0PZ;PJGofR9DD})<&H-W7nT(ILCvp* zu!&$V7$&nc*HvP#tf+uPU;KIhUKYB%xX4iO-_40S12BF~7)H6uyAKY>^ zN1zL940!r7RdYL->A3L?FobSviJ{mG@Lh8(PFg*m6PkDL=n!WrKz1TK5VvQs0lvc2 zk0np{DBzaMX(f9yBp{|wr#yM6HUGhNJ2}Q<^+o$K5Kk?GiaYcxyZ{w%Asd7(!uZWL zZ??E_YME!|7DL03%&4( zb|l0KWYKVN$WJAOrv+pDddmn#`OY<>ErRYUJ+}2WxL|0B-i%Sx%5zWcN)EkgU})^H z6EZz=Uav0iljA>UW$FI2TR|U^xmNR|>Mb*fVof`KdmICUXZhb?S^~nw>{DqueVpuN z9$5}w0TW1F2bOgF4utB2s8EFHeG%@aV9ZxjNKpLy3Rb)v!Z9xksx3pxCX6&R6- zAY+&j9BW#7Wu^qR^k#x?1qm^UkBG(TX?J6$g(QZ{laiztdgm%A&3!|F%wAHTW7c4T zpu6p1D;*3W<(!b`AJ(%tBXHxhSRsoG3!1Y%ru2}eIlU+kgyd0{mfSnwFx7&-6-|a~ z!${(VInvFeH0X~G4@(hJQf`G)@X1KFQd#Xt7b-2Y6pPh-n*e`H9Vn)j)~Jg3+W5FS z%)aAyn`bq`K+nv|3I`lnpgCI-EB=9>Z{*b)&M(Bse{cqS(cYd2w5zr@8h&wcfylUt zN92fgXu=A|qOrRePY@A{t!x` z44}(vGY5md5fl5tvKROZ5jq#n`@ibo^c=1c%cr2PwGnHp;>r3&+wh6X6K z*jEQu2n09RaaQjWct7}?C|Frp$$hl&Qh^Ey>7I*8dbyObMYNQC0;OeUH%dbwLG5Z# zeeAJe-0uB|rlzJtjcnJJ2{|TLV;FHsRp7KWVS~#2?nXvX4+sP*hz?6ic+Pt2p4L>a#OcQrQ(#9L727+Y9Y**Z}K9b9<}ipx47s2JT` z7g)YtSn=vujXIjQk$_xvR`BI(%Nj+6`lhR9n7*4fV|eF3<(gNDdaJrCAvKyA#IP{@ z{b8d|+>Hq%*x$M~m~zgKEZBa+F<)Q5RgINJn6+_W*YL>kJ}#>3RpWZi@HT zpmpqTIN|{hMPV_jz25@+(s(OS#M+++xU8MUiurEj^Kw>!$kZ{SF||NmP74`0o;HvD zg-WbV<$sdNJ1iv+<>>!>a^c}#4^<)~6KWor&7Q%RrWQtzuO=-^E&AoAz|X3h_5I;` z34BI#^7+T(!`<5Bb#wWCW%FulU7Z>0!%hb_hcKaz6We5jl^(U6Fyg@L1Pp567W=!3 zr!Tf$G9Px8({e}aZ-dQy>VKjwIb$CZ@r5|8v($?vM|}ae2VIYyR=Fa4BCx1XfxLhNN#CXBhr;IU&$pL8s}DSnJ!5O9k`J2! zN%@2*rJ$1NnUY69Al1L(xhBqb{>N!%XO;0Q>jw}ahK@U;G(wRLB#%Ee&>92kc-+Dh zYJX&1eOReoeH=fz>CtW95LmnFkW}CL%_>C-7gayGZ_9lgmTWoNWFSf2jTZ8|Kn6S^ zO>PUGvC>aUdXK&E--8J;r1Bcvbb59itd9L5+g=iW5SKiyI=O9nfmA^UZ6r~1#7SIl z^E@5$-1h=PpW6B+%HEu=UWlz;F4F%sRR|$BpsKzgyY>fEF>xBQt5q+DpL&9@9KZhw z{Xy$9`jzdMrcmtK<#Ix*xX<|HB8Au=0+$S-t@ZE97DNS~D{J zT`tLA>&xVq^b`xP(h6*BqqZJjQ1t!x7G*@4 zML|^R+WEKfP^HaCZ6=;f+jq)_Cxv|SgQbPTzz549S#tb=IqeC;AGvp|Rz|M(HTL>h zh&TNFt}`k^eh1C|2l0&WTBily>wb(X?)nhz$VunUHB;Yq@|k*jndJb1*QwY9j18A^ zM*XS8{P|l`7sj{~m&EVV!YEITZH7=M%42g%@i`ebW_HG7Ytod-{9;@7@GIe5HZ#m2 zm+I9n2`(kr#fT9o9#N*;NB8h1W$j0Y(FydlNntbzjITb#j(#wjuNtrrW>KSrTT-Z( zRYA(Mde|aXz0TpgZ2b3Zp6M24qC2l^(a_1lh{P~=vcK9eu1_<2zPJq91%rEnTE7|8 z2YSL3_u=55Z{()FO((QaO5sKr!|E%@&0kaQU)xwuQN5s0R4LLR)qmmsoE0Jpqmfak z$9T2+!cqqmW~>>sw0OkzarWRUO~o3D&?B4NHe%ZLVxy<~m zO17`rGk4SUU{5^ddp*N6mtMHnDI8lCBZ%KdRbBNsxRGk4qNktYm1CZn;3)1uB;X;f z)V9lF{(|OXJil=-3!^JKnFgl`X(Ayx?|Ig@#UUyb#nkP58yn_mZur;%MImx4t54^1 zOWU0*hv}l%Q6`p8PLa_d$9*fc>wXgLpsg*1Fgt}tnit2bZ;bkko~p$cs=pE-i^?aE zw&~p>Nmko7N;~@(9;Bp*h=OdNLl+p=xvp+_A_5Z6dX8X2<#T_&Z|$bJh{eMq^_TR#!^{TaIHeshMWVC`V|}v$J3n(aVEA))GKRhb-KhA{4Av zeRi@@^onE(u!q~brY~p}Bk(*8`Be!)*kWHiDM0cVA=1#=;Ylx5*i3pAD~es7 z?R`wn5&d0Nx{!Bco!pu)CHrVv|zgIY=EQe{& zPg!s1G1W#$kzg>pP!@QOyK0{B`v}rk9uR?s%p;OlI7gIs4@7ndXq1uzYyISr+H;4+ zG>uw#7seDZZ`VQLW)%%0pI<9yLe=MsT*om``w6Krsu3i7s4@oO_CMCf- zi6(uj&xlThErV&=8cF#td}i`#bKol0cQ3p;EA%Bg%4OUwsQNp-q1gMw5Gve@F>e#u z4_->`2dZ`BE!*4MOAc*EL3H=);l!-hM@z`L=FY2>_YUcqne}M@E{vhz?;e1s|WhLS2Ov3Lv5R*bf?GDDC)MKdZgCu>9t;)6*sTt3D?t zHOcz#4NdHET6`BO&G*2elD*c+%Wvf9aDC_3o#+1*h3tClDU$9!=j4XVxEytEB6{b>jq$<7_ltXCgV`YY>P?>086gYSj6BjTtvChHy^_#sa#V&XWg| z#~aB^_DObaQcn6{)LSh;_4+t#Th(H~EL{OKv7~oM)g)$mR0jPbkBC~J*NL0|-RHY6 z{&X(Bc30(?^O9O>xD1my&ycz8Uxe;GklQC&^?5kt4yioYO5y^VYLrzNCxP= z*<@YI^mqLfoDea;+skSIz<%sGdDuBRQkmzpantSCNUm-(PONAwG5;;PMrVM9)fCfhk-6vy03xF&GC)RFf{~c*XPFcD;P8&EkiicbxC91 z2h=QUEEiir>;ioJIHKepp|YS-L4F zyOa5V2-VWr+4<8a_TbUpriuh%yfE35htdkUZ@RdI8ehCSjPf&o zF8dcE80y<}2uUr-U zTTnxLI~S^}t1H9z$}ts}s@#zE50)+wA~8R+TWReb2|%Xaym%llTFr#)BS0KUMhNou z!UyGi{m=t}grjsb#CaOdLK`}3nIR>r+3K^EWA2tsTWiu?VR=CXJ0R~4T4B%iZj?e- z4RnQypM6I!V{I1p;sNSxWfd2g~1Xh?{6`S614CRpW`F>f1@G*>93H7e%}1Gjw$4J4#}Gw;0D; zfMuqdoSKqiAQhe75@VtBajqV-v`E{B4mW0^4p;2W&QG>(0Zwe3H)en8Xhn8%d~eeb zyfmy{rU zp63p*^>)Qf?#z%ltB|4Fxj}hY<zrFGO9#rUq5IRFSnI0cl*(0&_}6t(I% zJ9>fT|8^N;88>?R!o+S8M>rvM|1SBBe61ZExPuAna*#m8U2HJn-28m~@`UOHdg=}X zRssTHs{jCT8?O&Y8~bM)qwAS?=_^06FayrRvmX zWoAYvke7C=fji3|6BxQRc|Z%tW+`Q+Yr4q6Jw$Jz_wYmnp$FV`KKvD1~-p-yhy! z`V?tXDpqBLjk^!ojTSnN6$2z*7e++vf%aDY!3IXDL-U)V!Su1VTm}<`m-1Wa@$9(t z;&e!vjc1mm(8pJyPaaR%;nF!5+58huWYke6$;K5+Fu~l;?o7det9i179+t1y^KLgr zcRmiAjJ{$;>lIwuHcV|9iw|cDZaPfv|25wpM~-a_0zCxQ8lOgt)v&Si+`JH-$CLLc z1g5wqU#WKsO_Gq}4RupXIP223Ak4okpc(p6^oZ`~d@@7xdXndti#d;nqobn(lq%93 z+Sd$5{?>oSi;l3GssB+!yKh`MJ)SJ148~6>3Q0fmU!0$x|0fKw?A%Uyk54CY57b7< z>5`{k2!0Pfdhsysg5+zNNCR;LDdbVGF|;#Q@?QM<9T_O4h)*J%>b5cxa}Dl3FrUa; zMs#S>Gnbg{nHrO{Tn`OJ!ET=X*PCercFzd{PGy0E$$$=T#dN=M?VluIv3t{Z(c!j< zH~%no_cSwpn_?=c-SsjAEp2%Epvvd$%slsQIQKRrjMjLm?7Y0{X%N}I^N!$X!aI@J z(YNoa>*__9^qaA`#KL~~E-8r|&6m;yw!%P#jD!Mg5tFa660!T?Lo_C1J1NQW!UH1t0K){2xSDeOHnKBS*T<>z|GZByMji7bkcOFCQe&W#_vd zNjnw*DRtq~rOEv%fh3u`sDPJ?j&z7RCqx!=-_^yPYXHoU4+67$CAn>n@c3*-S3hm6 zc>4X@%%!$){hc`|j5})?T7{~KiaLxg@`;9}iD4_aWk_&)n4Bm~?$km*!L?)9SOCEe z;cv@A7+}k4kdV`eyrOdO4vCn_!xFtx&6%jGsyIO#Hq=x4`cNjz#%J89*G77D zIzBwD|CYKg=+=C8N{(-rP3)?x<1a|#E>uSr6U|9K%@{KZk^Drokv6nl3)F{V6_3?@ zEGUM5c}bTuWGPzzhADYxOTApt6Ha4w=`i*x@rBH3y@Oci)JYsqTXheNS4ZQ6XDy+( zuCnQ;e1sGfD1oYdRt|vIaQWb>s^fam=cL2a$7iXb^JU9w-C|pX@60vt9(G$NsR#lg zM}PR6=cQ0>zONMKnA%EGhk)3Qfkg%B3e)*kNu@ZCt2*m9ftNLx zA;HgxKtl(qRVm#z@mEH3#(dp{4QO_NjTFP5mmnCnKye9MUK4{hio+u4?jRkO0>M4u zGEYj#*SP4}s^0^befHfHMX^ZQe0D;Hg4T+~oswlNDRlcJ#|rOZGc<;eWZ_>@QV?q= z%*CWS0qc^Qn=2XU? zk2kaxKNv&h`oV-?I0ufG`Hk#sac_8ySK7F<0JqG9@-Tj{wQS8%s<^;xjWm7U^J{Os z#-ua4H#=`;u`cWoiofAX3fem_SU7FU=bQ`=3SvN$!R~%=K1G{lUl{B!+EST~+$Rud z+ye2OTMI^_S8sY4L(ZSX&SJB{EVE8vPWlf-)(1wktk$4+Cd@>gVeEUrS`SAB|OJN z2+bIvFYT}P;cs`%NvN-bI zWR|>TVbP2gf}<>#nyTb0BDn0QXrn^jYbu(45EH8x%*J@LL?Mh!ot%qPc)uv@k6{b5 z{*IFW-fZy)BQ$KcU~Y8XP+s9&jI{s}@zT*ss=r{joKaPQhTVLCeWH#+tD1N0oBZ<% zF{hcj>hF@hr>nfs1A+2KN2a80_ zugDLJn;NB{7-Vx?BXIAxvdg^AUTd$;#Y@B|`f=4^c5O1;I2uj%0wiE`B8&oMBErv7 zn$Fm9RzXRS4>7;A|1rD)+aC;P!7+cPn`3(!&5u&~Zpvl5S_stPKW8|>loH}>oGq>^ zuJQb26RA>FL}LIS8vd4Cp=NOO;yYH#MbbH4JDeWAvJn0;*rjGJ zqrZ|H<<~Q~f6>>oneBf3(&Nvj(cPVu)mUo%Uv{yzDZBqWTu&0pK>L*TUL$MdFA2$A z0)`*cQ9&paUxp={zL|X9zr?)$a?Zk+Yna$&+Nq9Gvy%3a(h0Bgb8BduYDL#a=fCS8 z($Hn%zV7yRbOaG&*x}I2ApT&u@;)v`!++L6`&GS2Ky_U2_4QjeV??W3{$`MO?8^PO zvH5Oup0plR-JNtjd98G!@(OovWg-bVqtrq1Q0RIVs?_uli41&{y=TkrO!nz8Y|ZIm zJMdXk)8j^cu3A;~e)bzw8WVp)sTp{@#Ko{=+oo2ttU>|Bt1hFWSike}X8#fXwa>GL zjuVIZ7Zak7hh;^7ii5rKvryC(e+!>=J^pK>^S*~xSYbqV{X1L;N46W z0xp|G=25r|yr`&xHX;*ygea!3VG2?htZC6iTPPn~x5N_)kl9}F)Jf2u{oe)n#YOStumHQG|xpZkL5Z|LYafH)rQywr@l`dT%~n z_^`*sJpLXH<{s5-^~~-Q=lqjdZ83U2lcb>qLnKBX)~N!e#2^+Ic8|yua}not6KhEw#Nvz~6e$s6!fU*Vq!Z+{gcfeJVn$r|#AMvn#oGy(JRJ^z1Pe z7|j+wQ6f^|g+EE3ajWd^f5c5cl^cZuN)eZmB`to~XnSX88KUn`92`}XwIbuY^cR#o zoC1&(>+N&%og@4%(Rqtz3YEk<`%-QJ^mk@k6l6w1^xczs^AVrTM?RS+2c-hi0+_B5 zj06K@2yoi}9m9a#(A3qH`%D$@!@d%COMjjhSj7MSJL358kfJwXCmmyF=kH(bS#Y%Y zpM?sGU;907xgM-e7^Mg3!NI#|n!A+u2n1hxH533$XhYTsXvMGTY$LH)Ilg9*)y zP`iKq`UV$9#~i&7drJuZ(oo;{3{+Opft;;D=VdU|kcM4KXwAC#V7)I$kK5NJMCtM|g)7*hLYz z{g+=z8mOY7(Qh&7{QrGm@;kDvW-YKe`;Hth&>?QDdiXBb<03-*{6VGVC8=<>3(CCTY#EuEvTg^qOCa7UI^K)xYe9&y z*TBfR*weq2siNNk)cBo~upjBf<x_LsCZuG*@2U^ z`f%)7zJL2Sh2Uo5esqFx>VFY<7dbcjpnuC&64@3DQ%XF}B&(;PLZI%6$7QwhePBJP zvg3sK=25D6 zufG8?-LOjA$ndWif$>zOzuKemdc(sBWt_M#R%4qywFQFnz9M=@`-C-TrK<_SmLdEo zL{mC@{Tt$c`RvF=`T0ZO9mbd-vgVl-9GvwiWvLy0PY;ipt2$+9jO0}m(%;4g zv38K%J1LRQENV|3vPii-Q47%V1W=ejvwV zZ;OafPYOav_&rA!u4BsaMjugu`~=wNb<1?`+qcjbyU-D*!O4z zq)R17Bi)^&r5RlkLuo-8q-B7BNRI9pB`uxOA<_*>r*y;f{XOsdoU{Mnj2*V!_jP>| zVa*8_p~S|RFI-@t6(@9>iOA!EOk3u0*S64@o z(R~Iq1-;;K4PQ>1#z0tpnQ^1q(uXdhIXXKR{Rgmx!`0M^Gu@@oP2$DTJAl6yzx(XP zbGx7Ki`fA>KYi!Nzl~l}u>N!j65x+Oc1GmTHx)MPt!<%QL1j8QSqOxuc|^}!?78Es zglj-F+x&IBOS(?9tembMe%}PSvW-i1eI6W~Ws23*)Y@p&$KFH_?B&Nr!U<47suhu} zVW@yt1+4Mr4M^rq9`fPS>f85y* zI9=HOfCFL%*+(WCk1?HJIXhb19PI6nr6-J#$fAn~Jda?m5uGzFr0FGYbr3Lq>>C~9 zE~=bm@+pm#0C@vRNLPsEd|Mgm!?OHOFEvgWn z!vJyxVgLU1nt|;rB?H3jStlE+3?vFpFT);DOdELqcz5KN3_#baZ#%#9^7HDY9=v!82mUHg@bi|S}loc(YneMhw)1`o@SoxF7Yd-b@JZ_s|1 zeg6J7>b1EffOpL9vF?z{a{e@ZmLxO%mbfx3DEGL=b=OvD_2b!oG4Ab$j%LTr=qy*J?^e5bhYoavHK!dpIW=;gCa-De;uvr+ zV}j*!?>lup5%-A{6ja6+b@{z=zk55ZEtfFjpyqXK``={Y^dLX)3sJ{`Gd z|2U2vH+R3NuL@iqt@0mzB^A)$FYVP|>^U>rdgbwRr}l)wDdjmJQN0x8jpL+kUS}{H zR8XzY9E=<&2p#3_3DovnM`$iaMO48&`x;!*KzB{8G0PLnow~zLSV4r{>pcj>Lv;k zo0yyo=yZi4Z3(+?0Me@sXAPr?H)d1JqJRpXI+@{$jE`{bCsmL6K5&dXXpGr03;(A7 zZ1&nCSDc8qYa5^>BEZ0Xpa53WrLz3dbXDno4xihaCZmv9&sM>EXb$9`l8m_bT`?s>~ z(@y<5?w$Sm^2b{9@4H3Te73em#6)7Qqemn9Ty z&(6Yc4-(-6JG++U*X1o~9@QQQb;FkSJw*4*;bmo&4EM#wMc}4R#$NRZlO5qz1P+`YrE4Zq<4kZJ)EMt?gqPBb_&w;nanqT=P3dK%?67`q;1*)?nVNdl} z3TC=qMM!Ykw~w35z_aLjqW#7!ZZlrgxAV8&P<%Vkyn-2MGB`VxHy?*&AL`1U=pc%3 z!2Bs-@Un>%=8PNghGb>$|l zi`XSb^eYWt3r!i*g;}oW34N6}qcsZpKB+6JOD8v{-dE|+eC0GleWupsDTx9a1^ zLcofk6xQW;Y-H6)=`sF7?wxpA%k!WMOKsAI;9chYCet&$OLDX2FUSyLH5}_xelyEK znnk~CB>y_O;|rzrrl1ykaoY@vL3O4&g9iiNnp^RjsTTB@AEYZJcS;i` zH?UW8DK*stSI%;S(JR=Z%e}=m8jvH<%Ck*l+?~^^07nc=dt};5;yzz+_bdz44Tkt` z5n7eqU1L4mUHyIkARdS_E{0DW?hVYe`f;?*4`Oz3A0D}7 zqbzm`|C1Lbn+Uh|Zx3Hn`K%lt|96X3K4c2o+bcFN#Pb(2qSJj5M7TK)bC=`^{Ek+o2OKE-uiUbshnDT&3@Oq?hLAyt_tU!O>92ra-Bn zZ{S)mh+d3kqh<|7rclJ?h>W|L@YgG&GYF>X@bS};OUKpI*lg5ao_WA^EaP_>M4Acw(QPc(rKn`t`J$GvpLL!fpTj6|0K$g zpUR5<31dTXadw&}vy3ayP#6$G8N}FXUVl5IsQxXuuUFtrCuUqF=Q}7TFfZu3SnmiR2(><6?2PEumF!&-<8bs zraYq7Y6_><7qBlk+M>C9N~3c4K&XWIAQ?i`pfFS!E>gmpGA_Rx08JM$`Y|4On{c&! zF&|SuP3Wd?1=imcJw{Q=C(1FH*6>sehQteeV}=1C1`q)a()HE=zrdTP`})FZ^;3AF zQ_Okh>Se<)m*nlawPuFI$-&IKT4d!=%|RCyEoP23hZ%2DqM`?TR2G_%xkR|bBK+cA z-A!+P|ImO+-fL#cU!qt$!+f&56t}vo|8Dn%^kX}o0**Nwtxw%)fwu{{Q#5S;i1d>V zoYfuYXp#LedNC{hZT0T(O6p-m%=&SMK~wU2j^y|@*vlu*mV<@)Za;o-6Zvr)KneoJ zEDmdyz4_{(a>Rpq;l0xdYw|gkYhsk+c;vjkv1qulwy2ub#v@&~aL|eY{`Gz%A z`eetNt?(jA5Q4|$9(!w~rDU`Pt!H(cscUaXB6W3jA)qk!f$|WxnTd!c(Ug`qWKda3 zYI+-UXk&v=Y7>XryVqfg`SI==(DL^Ry?gAK3bGKAP&ugG<$?CJY{xA0-V&*esHkL| zUb*{3(|XlA7*xSkV+HrtU@>r8~e=Pl5EXPaygLt-zGoO|ZK^gS{R!wk5kBv4Y*gQWawZ2=Ht(^6eNRjO3Uw2Gu(l3` zf4Q-9cc0ABg!tbKTi}EK^OGQtRy(Boel#_)fdN$vq|R>K{I!||>Lbt>w z9rm9u>bQimR02(BgZBh)b*3*a$f8;4th7lVK1pB8L6AuOX%YLxI%v`k9;Lx-Uya9S zl@VAneG~a}!CQv($IsmkwYCP(7&g?Paa1rYi||8$0AHsuq=c+r2sD+~K_6CMF9_v4 z%?hwDP~2$agGcR#7x3}9Q|qV>mg-J_siZ{75}qQ zq;Eg9S79QJ-~HS>0aZYIqCaoF9)&NHq919q^dij!1PX!}F=(&I?tShQI*((v#LqIB z@PO4*szzMnvVxGTM9g}q9`J=`(_{B{$XS(wuIT#0K)|QqHg@4F379Z=2$jXf7mJvV z+p;q!mM!2EypluN&}b&hUbvTC}qIx(RxLafcEMY$m#=c z=nPbliEH?G9pz$BUGG45bFJ=&-@pR;)&NR`PS3Dq<9j^HPz`@s$W01-vD#LG;J!Yz zM78BgPMp#&NWLR~nsI>m+YdY|kk)Tox!_#j2AwGnCjvU~=hM3p+oo(prrX%2Q7#fF zr2qWLV1)qJ&AXBd?QKee3>O#lpvpyTf@E@YUMo`Dtiy7uk&u0)4UF#a>MrQLz`~28 zsl}`}ONFb)Rh5;*6TxU$ZC7l9=W(F#4K*5oL@EGM7mpH{Ncn$S0RPh|FpH%BHJHd} zy3Cx+y4-4X1aK9xf;g(U5Z2X^rD@8EoopFzI|}rZ6X1Nfiwv$TEnJnvXct4ZTdIXSOv1dG>f(q{OyCbnW4CVW1q&T z#NV!B-<(=8{sem*H*Fu(jxXE+KfG-x_|YI)YSns|C9Ys2L4)S$W1Wtuaz1LlbDdM3 z&S&7+cCgdWzpt!aUZ%>S&jXQm|FlGMDTWqtlM9IN$6o}?o*)i) zX9@)Ehulnq7<#;Q$;LEH1}>QBSB>%&*vySaG?^17i#0nQ4qbp$!KV}5VhzcM^U42G z2&K;u){~M-$XS9?^HCQj7!EP+$hW2TFylu~xo0)F$%TE%_$)6X?Y=tvA z2E(rG>f9AlnGKW6W0=q3rBOIVc^Y^mQL*gX62l~Hc)UC;5%SMeGwyU?Sg%UHvXksn z@!KrEdR5?*Cow6c_!d9&*1JdqojH!9XfJ$|<|TFzNmAY$5j$`hQkxpo@96TnEXEF@ z50RXON-~LE9Vk%9XvgU5M~oiL7?#3`#`x(r3P(B0`d5G;zLi$}fJgUi-<{V4v0lZZ z0yMK;bquF(E#IXSB`kDZl>B{{y2>p|iekC-M!-6(h{mZvNKs|^;I+l=pRFy$^0Y+b zaEY7qt#aJ`{I_dk`Ax;mGzZSd8;%efhC_`j#e5UPwB3)!3hN)3cdfpE$Wkm7a1eZH zp6*eD!>@GN{8ILFv2)AC$Sy{Onl!-krJEenq3y-2ukS89U&Y z;-#OKaqY$hFBS?Mns8F8%zZZwbQ`nVeYB<*2S>3~yOz{h3KCdopKhfeR%t#>TxHx$ z(B3`@w_kd6oCn@7{KQSMt2HY1;t*U4NWAXEKM;4xO`N|Bk2-n@{}L_qIkX)3&i-(f zx1BtF;>Matrr5a$B&hdiJ_1hCiMnH2>`Vcr{xwx7-0{8bGg5_90;AF9=XojZ=Y{>T z)IG%X-p}$|k36dGx+&VfH#Lc-kE-Gq-E$hSQ6`2suxSHj$f_bWRl1i7;n&a86HX7^ zKiHsefRzcu5KWMp@3a(jU|qB~mQ43AQ}N6VWS3?7js|do?Z3$w4kU?Zi14JWVUUP+ zVUM5+gEDmXX>iltO3jl{nq~Ik`coNrw?i)E6$s@MLLN|15+{)tmipZEmkbk}@h{egc+$}?@R#^gK8zNIXi(0L$s7MrP;iOZ42pJq%f|7G`JP=YqB*Sq_ zY{=i|3bGUH+AmA?m58QbEjKtl8|R&L6iOHw^ z*?vT1a1@fKN;kP({5CeiF5w{9%cz}FS5Zi8zHk%r!Wvk{w%SsOd667R=$m3rhgLbs zhRB!q&(<$9{eWND-G0rUoM;eP&aG_0{IE~JdZx%$cct+5&YyVrEbp%Ug=ARwGA^4% z95#oSF>~4P&z1Y$&opL*Tk1r^zPhnQH{U(ds9pa!XC%5%$vlO=b!Eqz@-Pd+-gFH2 zuL?w0d(U{pvYmzPFflHN@49QRooD8ddU)NloL-bB?G=JRj1zGAuyL%3KQKzqc3OH~g{Sua`BZ3%DoT;~isIT* zm2KM;8a!|(>N=r}#R@@X>>I##(tud)LAom3y@ZAM-M9qY!5%)}haY5cN1l(z#ed4U zOkgKLDIYjGXt!uz8)w7Vkv`b|`o~Sutvaafql$&nhN2cO`AfM!cb$ow>sq(cd=%^; z&-3Gx_VF_I5*BbsQ{hN35(%zB;y8(8eErt!x7bjaxf!2C!|HBuVgq%;jKMBFdL40s z%4$>;O}Q;HTjR5i5w-I<77P})XutY4LHf+l7zW2q;Ca>w!NXsZq00C%YKg1~ZPMvf zXBGIT^^;DA-+lSZ$eR)!_hHwq&gVph0)J(W^6W#WPs$YJPos9XIV4XPhOSGV498mbRHBFU@cxrS+bc zy2eoZz|S&}=7}5yzFC#!6@HUa>*D@%<*~zJ^S?91dVIg^Vd+y(#A>M`^;|IUczAL| z7a1pF+;BysgwgfNbQUd-R#?` zdWYG)X08pwhuBk5*oMS0j-55-C@=n5xNwlb-9f|?kdAgiV7{`?sJBvICb+%yB0gR( zS0&+1cWYII6-M(qwgZd#zsJCPO$*+xSCQ zp@X=d8k9i53x}iFP2Gp zC47@3XL0vE4Cx!%qSz&pIDGZLdj+)<3-mZ{=hxccHw|8&bJL2ZdSUj<9Q6iVPOaxZ zy&S&HPnAZ#32w5c*Z(_o)NxnnaueK`AyISt%YX)`v@a&vnf)i(b_slRyjDVtJPke@ z$ma38J|o4&c(!(bB``Bq1g0vJFgc`>11#{?qmEpOxED&(dLY^x2`Rtzb@z z?=A2FOCDeng^Q8CULB*y5MI6C67IMe6F=qXE=vn<*90Tw+5ZMfjg*c1YC4yrl@K6icXxDo}UT_z&IzZAf-aqjghsOgEkBq;>3n^%F8-`dI32!=mZ$kBnh3VlM_m zRVQ)Xy!T_6+s!m0cMG{hL8F{#JXr^X+EC~lz3CWagJ?nG!Z-b3e)shgZo`a;?`-jW zAXrmtJWA*Ea!Vqlz=|tsAgJ-MeCYyjUd&+nFmtqWUr4AjZGPb$Q0_YFVBXgJX+UM~ zt;nadQKv;uGaabM2G`-?7X?N=L85TNmIm<;r-p8Ro3AZ6>4DH$TC80_i%uiweVVQr zK?p&Of5Y;OqgQZAR8cpN*lg$s-wX8_{|mj*DJKWJMKcA)?W3Y}XZyv=?oBm7`aL?@ zTo42NoBJE3H4i5Xs=7vwKg;S#o^B=_!Z&bY{6bD5U)&_DfRORqE zs8U~qT};c5n14jA`TB~wz9;=-3csS*O7fo=btU-bM8k&-?J*s@_GYgtNVuK}m=(HA(5fuY$fOyB#-tI^Bqi`n8< zh9;G?ROYDXRfAIg#5)00t=OqVKx=O{Q!{00YzE|Tr;E45U;kP_wgAXa>E75!T_{>o z)P*E}**?$9=ly@%ieB6w(grrZe!S>-d>(&&{Z|v!&`44*!<29~V_x=cX*OSN z$CJgAxmYzOZEZ`P#wTlHymI41BS7_~BO>*b*$q!k~ktm*jyEVqgFRcFe zNP({T85w_7MMabN^9V>?ef{PUFFL9QmAE--8e8GKw<);wXhp6_X!n^vBy6ON5Pf3{ zN*3j&_NBU{cBC17Z7Nb1$boS>+Q{htD+!bGx%d83jr9ua1CcrT%Fn(?hJT5TG3r`Lo*Y%p6=$8~8n(1gs>EPs^ z`6gy;VzQr*sNnbB)>Z*(O$x;hjdWi-b#uR3vh{L_cEODx+>>PhYkLPfPf0kS%r@}X zmeoeK6nUx^7;sxKj=fP#B&eb#&b{by&`$GoFc6H8fqmS&{@~T2nBndmA@3DdD9dSkDjuw)a)zcvusYtHg8n-s`jPxFZn-)`pO(KQ-lB8eZtDksvO zM?oTGfbylZQqShv+R27Y1QD4V4?|jlWEv_*!XTt;drb0gXJOfF#@DIL@Abl-mR^2x zjOmNI;DvM`ogQcoV0bs{nHJu<-<+A5S-G!~E#;K_pUSNnXO`w3hd-7|R2B8vwDDwy z$cNt;kH2P@w7%zvnvD7UD*rYxH1v{)y5k0&X!+xT2vOnGUJSbBK%#JpiyV{7QUhl?#D##8V+GO&E)ol z@ga$-{*{09S={i}`D>b+~zQi6$SmIa9#R_MimX10h_QiP?AOaUx?#>+i#0dN_B4@>9Nnad^AQp@%RM! zraPEbhYy1=@+)(T^;U38wDMR*i)!09;s^Ti^2SS1C1T(z285t((GnPnI8a5KW6s@u&5VxrDN{92CQ0bCE zLk25RO0i2&4@QvWwpo9W2uS1?&({&%S94@5rS>{Fp`Ht5d;63OqOvg#vAG|V-=2nu z%cHW=i#JzPR7|0Sb}dXK^k^^rsCz#>(8?So7%tE2j`>Gm-Z8nEOPrA~RYEP!9~HZ) zVVV2>sNeAd;md?oxR+74)W8VK#H!OhoEE!9nUb{Xjgoi}2l$v5 z=0gkhPJ5&@VR=DVP`lK6w$EvBx3Qk2^#xZ!Rgb;i(q-~6F<-}+KIp~4q^5?QncWw{ zk&z-ZjX_ivP12G`GM4&MmS}qX`BBbN&wZtt)zb#yj)P6^>`kCw`F^PC@sT#J-x_?Z z5*b-?V00^24SZ&fTf^J+CnVBYJjp+!1{Xr=-Nu-@1?@idpE-(mjGCW+&b06FKNdZJ z4^R|#rAB+Yt{aQ`p1;$UmI{2l2eRn>gI7>3VP$1y|4Fid%z%LV>l2;M6^fB$-DQti zGXP_K3dg0O@jIwSB7unf4EhKJTq8r|69kaCvl&Z2zpXHoSdel>B03%<`OUuFPks zujmC$C+gyP$WEow$ETOIM1IFECAa^?KS@Z*it!icmt33;o8J0PS;bwd9Cwr!dYxLT z+p~y*BfX)GVFD&g+4jIpb1%e#9JYrf=nF!#qe1~*qOLkpJM{rr zn)dMDwwcp+E{oSe(65Loq2pA_f@I4z8RFz6OkUJ@N^L{p&sIh--B9eoIeHA0d@Dh4vPZQ?1rXcOD>x@7p@>muK(~3&jN9JN+e7&P8 zjHfvLTNs#ftZa^_+__T7NFyyIiCiSH95a{Um%1!9VWP4W;%l2e()YkFm z^yA)<;X8#y`)IcX;s;V9v&0Q<2dQwvIg|)a8BR$U6L(!#iZ#6LQpLx7qOO;;&*hArPz3uDeaX7V~6|d!OHfXwf`Up zFr0`~xX}|Ej1YPB%7!3DuiOS6)V6&riXK)UuBO~sTa#IQOyg#ET{>Kl+$yv; zI;JhsjyD^vVLrx}D}1o<&=#`eAyNJm{EuG3tX+{q;+cAaTtQsCeTl_ETN$Iw4ygJfq(u!h)0)TXIGn|RYoak>lrS&A(I7a z0_P3#D(?e+GAmAzR(w|%+pF*$Om4iPK4Q5sV*^2v?{L?`EIGl{I$bSB07vOFRX3?X zim_O)Q|>o7SWk=KF~asV2IH5&2KLzWCAoCTHNMhG7rO7diZ&!P1uk9eEO^jwi;dY_0=@;s6j9Gtgq|#u?4AIDZut0e%Rxyzc)dh>O?ux z!a{MzRUt!UKL-&()?`xXK2YwdYLTFR4MIOCXmye)U&nmX{}+4wHOV`?C0pdfpWa9k zptSM+7hvNW)ckY`r0v?W%^hF2tN%iLzaGM{B$QEwxC%f4gZG)v`(D}QFN3l;gIkut z-FTSTojfROIINbSRky8JJkU*i#N)wGc=7CK2LmtT_cMyG%lcz!Z3*?Zo8!pr#*er_ z=w@lqQCDsseG4$aLuu%4FHSuy|PLv3R((X#F=;?bW_{ zaqPEx*Wk4X?69-}H^ErKb_?~BiN;~^I$fo|noi$#AmBAfA*!(jSlT5c@AUhcw}0G@uPPqL1+s9r%+ z=8?L1@93$yBcL`(&1WtPHAg33rXlVrbp3Q^+mnL^Cn zuFL>2`YT{Q^4Q%=db;bO^}lwoaO#iLj#hr#tUsNoQ)1PjE%_bo&e@0u`qo^!4rexL zy`;DIx$#~Vw?#YnAWagd@gUqi;O1&L-P|9yo3(ZFRILcH{rvoXUT;9fjd z1FkYf-M8+;?W7)0;w)Ov9cMjyy6kU!pimagmDAhVjuRj&?ZdP0RzztQ?dL%4(9n3` z;~86_+$}dae}c7dPGHnz^>zkuvt2;W*Fp(C0eqm8*U{4D$qeI@5Oo{j3-7Y+#vNg#V`I-A8l8 zKHbA={r4-pJ}#e^+GWM^t^`~ygS^-_Gvujw&g#^WQcly<5KAx%N+nI);czaA2Rx#~L+A9n zJXO-YN<^AgY1=tfnEo`R?{sj7W(bwEw#7$UI8+^JtLr(T~5e zmBVq;k2d1z6;)N+<(ofUkCsHeq}{iKz{b*U*fRFD$eB>~u&+fVFmofL4_Ctml{{w? z*8L+}sIw9H)I!6sWF2JQ)F@qH0}@%BZW$`e3`bY2t(Asw6Ssquiz>0DKB(%jL0uN^c z+xD9yXnY5jkK3-yK|It#7nxowFXXPHrJvaODXMpLv~h#m*(}WJi{D>r@XNUVXjb-m(ZWP!x$Njdg{5D(yvuU4 z3n^{{fc2NR@5Gx*E_I|Y;I!ubOMYB+S*vhL>lfk?1gjfI5sGC;?Ey}c%l$1R95e0e zUyf)BYBVbNp%p)Hu$vA+GLoB>&&Y^kFkkcbO}y(JfqpR;iFb_kVuK^j#?1w4laiA7 zSo$ex{ny4J8{+&;IK9l z^|uKi((Lz^fF%nRE+L}Qj56&(TZ137R2>)fr>o58v2)u$?y7zrN<_h@>LRs~(bE!S zdcDN~ddFkHBZGrMuJ2K(W`i68fPRD}Kke zTcd}|U6%jmNaB$8*kmBUCb?*`IPWCz@fWubdPmdLK4M10P)GxZ-PP;@E@3} z>xh{GszWhvZ-#rRHP36kuuOO=nSD#mS2~dMaCGOsC3sk+eH%Z}w4=L~9z`A!O~xEh zM$2V^JO1niE#xb~flGm;@ZEIk9AXbI!_ucHo=|a}?i^Xq?+n{L5ost4s6mnZ*t4Fn6p}8C zYv-Bnl)MBT$Hum{2l0+GakJS`SS`V)f1%+yc?a@nw(S3>1yHOm`RBKyOnqsf;eI+7 z@HN)%d$G|gl+MhlTMV)v;Ue_nGCH_RP!y{u4$RJl&M}S3_wEj%C_!;lqC;e(AC3Ey z#(GwLx6_ZC&ceDo(a3|nQ;26rYzI>pWl2~>eN3x8+3l0WOH04D{Dd#n$I6T1;%+V} zy!GN_7YT|dgA)g(W~L%W!KiN)11ZY_iOOVKz~)MPZt z%CnjeW4P}hX;kR)zJq>ej zv@K;TrboDDl^h%u#*EIF>T<)?+ZLJ2KF4EsPF!f4ZF3!p{@&H)_8pIVT8#!ZW;DgJl^9t+t$3Kl+ zO2YmC^gX%wIv`(a+f{e*-4E_0$9oQdIUhb82qMDhpJY;TZ4UMDa}vWyyAU9^N|=Dfe{D{^~|u~S()lL&&?HUrb60OTty|Q_H=HL2jI?&BjKELTp|Tm@7MUS z3yu}hSrL8d|6BCl*Y!^gML41VdN9(SkEMNIvZL33IQkbJ6xvmv(qps%5-GGXlF#)v zPA&XG3Htg9)_JYFlAQX-6ATP4P=wo^>3J9MRg;!Y7`{qg$$vP>-o8*Uv`-V=ArOeI(jJ9D|FYz)z`CO^g zn>97tYra{l-Q+y)GnG;*T{ko))+H}%+PM6}+w&TsL%$@SSX{QIMYk`=<`p;%t5E+< z6NM3-xlZoCeL=k~Lt)Pj8cp3J4Sz0Cp1>b4i~{21uYqkzfho@1SJTCTIL(CxVye=l zV$6gm#EY7N7wtYH2gBKx(ueJ?KOVZDe!iX6KH}(COu4m}Qo3mTHLm^OZe))ajZK!s zm~RLTzd+t|hE`_ZF3odX3{vu^8xWI5HT?X?TMPcYab(UuMjj%pZ~=iXtNA+8Qv4R? z4wJz~wrpC!2xCXAR3UO4K|B#RmFVbS#j)`g42oU7h>jwH{FF%F1&5xvyzY8%YHwcu z{^;T&EU{W>XD3i>+lOJs^+C_v!!XYBtHbQ~zynON78FGYq z-+!z=@S_cgCX5a<h*me- z$@6)@axHCm`?!h;L((wt#~@#YC<16vM{|uIIytNx-UL9QNh&#kO4jeW+d9)m;-l|= z<=y;U39M-?=+!;naq-r?cxY&?{IOD>wff8j5d7cnosB(P-CGN!xEp@h16bSt|9Hen z4W`tmMoq z)bnqs=-{`3UF-FK_Z^%O*mNrxLVbrxTqr~fvh5BhrbKMH6*b>|fHQ;J^v72n`^AQX z{FFw+8(0hv3gPFeBMruXc;B^Ip@Qfm`EXZIQ+kpZA1P*R{Vl*Q zrg!vwF@Nf7e0&J}Ds_Sw&y;ZAc33^X6mCC!t{aY>?w(vi^^Dj^N0~E`OjXN-!=OkwcZgt zt+}%m8l}J^4(v(sx<@7x*`K-E<*61}3;n>=an}7UhHqd%;NyGNM^t6fqTS2HUKs}T3P94rQ~hn3lmn-dp_Q-IBaKub5@ z(jAM%YlZvnpH2^NbGL3i8u&jFNPFE3O^+eW%w2EKE9{)HUiiEc(d=97xQ^}+S!pgo zstdY)XJ)d><9OR4bOwB+s^A!0IDH?l0CoTT?QN5+sFnyw;;yWti%oPR`VO z#{;70fR2e+e5>mDETtj%jt5{|ZdrXe0&b=NAOp;QEf}la=jgyXPSV)(V3SE3I(^u< z`j|G`x;28}AD<>QM(1r>8Yr`P*gJlDJbr3hy=$v^;-22`S^sx`b4GjnE5iI=2*uKe ze(L`=SpcL3M%IM}YCKE;)m5L|$neM&vS4=|zQGEWja1@}U{Lc9hVO|r`{kN4dPX$! zgAgfY=?Fd_!qR=?0T-!Hq&EFCaddQ_)8@kSzinxB;nV5;7Es6T9uYH0+h*=VBI9T! zQIeA;HucXoUd4IT?iQ+1ECsmmRqoG~

Q}DF72;ekkQy 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 E=!lak(+FRO8?BdkEkJ+{`T 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

7T3=eNQ$Fo7L`56LPwG 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

ENFC{bw`HUxL3|c>NnvVg(zT5pk1H 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≪YhAXN_P=jL7q_PPh2buCW9U2f z_-M(YltLE|=8V;VrNc{k_;p6dg?Z$jfxmiYhEt}W=0D;QIKI4e zQrC@oT{^Jtk;Ftvh6|MMN=Ds`&*C<(Sq4hIF$<~v7Wh*Vhr<$un%Mqfot>$WIMFhZ z;&hWd-F2UW_4?zw_L4bI^V2u5v!=7wsa5C{P8baZo`Gk5wBVa=Al6R}<{Y{WdsJ8zJ? zYLL5GmAhFyYv0rg{IlF8*0!xx8?dlTM@LscjXLM=56hN~Jg()D(QNo#@%SebN2IJ5 zq=i_#sDR|n(-;Qa*83W_cldF;=8av}^+)X*MBADgar#*$r(3|nLE{mhl4i>uAv)G^ zha$IvLM%ZvgNB>d>F}(B^VaT5GqxD3xtejgw678)@cUD*2c$INw+}jivYa~jW#=0< zFV${Aw?uDndvFYG=8I0brbf{AeIXd0iX(jg^}0&|%Hr5PV*j}|qKqf8ldLaM>W@ z2|SSxJ8^Y!7;re*v(HHt)QeQrst;%u)Y<2mSS4WFCoW!-Dkn`zhHE)@-76&`-JBl| z7bpDKa6ldK5vVm2v`Z9;Rf(Pa3*5$e1DS&-U}N9$B{=EQ-gDM{>-{@0-s`2vTMR55 zFOazq+!sKUQdN&UM}~&ne&tJ}7pd=NVpJ8h4`h`|XuLRaiiKlv0xq7bt0CvpmUx#q zg@m6or!gjem0-nCRQ>y9(G|kQzL=W?|MYdO zj^5ZskiIx==f2kd+h-4JhqzZ`-TvaomTpl=3DDr66pKd$^RIc4Mpgrgdv(Ph+q_$I zRMbCzs6M4l9civuy7_#6BQFs$*GMuFSdLhR^^KExkF-5{&o@}=*cp$8QNe@?va2ub zg__!hU*#=U^8YL$^t2uN=hDmHop-r)enb6^_t^DkqDP1n^rCbrvE^l{@iSTk?O=Za zI~ozmVIfz16|~ebF*hpNnw{2klm!ulo;VKW`g0sHga{o6ay0(#y85;DRPZ(+gf4%u z{idT_*0}$ga#QD#fd6J$RBp(-nN!?by~hWn9BcD6ks}4{Sq^l=!MgN=uodPgr=zbz zgJ>q)=*}L5Yj`Z_$R}CHB%heq{T)Uq(H_d|zW`iJc)a*w=M!O^Z6;!^HWP8k@y)>ka%pX!<5Pq|X76be}^ z^m|%rl%{EXLi=^?Fv6v;!)3BWIs~Zy2=7o>6}b`1-Xh|Kf_eHUCaq#J@vE zLHs=e;h9*}FQ7o-DB}cMp7+Rp zM9cpX18mFw^~d1?+&B%;?C@9|;v;2d^RsVv&3)FD%JRi_rY{bPd z!KXE~E>nNfKek&>*HG=k$8!QM&tnLeqq5zd-eDUZbxG>tBjgR0-#6+iV$WFNSW`*vIf;k+8w`Y7NaBJmu-L>`MBxbwxs^Tn!&ihBQ2 z;GekbVpE;NCse7sGRb{gl3DiQ~`>Eia!fyTRV92;R|yhVv2ug-&K z)566`jEvK}6H>QJQlh29gWv{!!`O@^BK#g)m|IJs|HaYy9qv`sbVs1;F&`0+`nTpf zvQj3+_RA6#_Eqnt0Jjzi-a#R?n)`a-I8tk zB6G_p;OEbu#J0(WINyaJSqiB*p$d+nxdiR4O!$Pv{FwBF?@7UzEOOUPAKR~~^UqUn zw?l3+Zg<^pPeCrP)py?W>uh6|s8M9ty%?7;m!Nf*4|g(Hf(L3$1DIz>JO$Ua!T!Gz z6HcrAg2EbS;O~!FxhCAw+@p^VgONm`q9%OyP)3UAYyOI_-)`PaB^1ogDsSe{redc= zIC-ioEO+>x_>0H049p4{=X-;-Xfw)yz*CCRVg`PrvGxYvY{!An_4V~^_ja&e-rdZ^ zM7RNqZ9fGC@O|(6tICec$^o142a7LWH4?m>X^tL&x4;#k(2m=8X;1Q`{l2pJ@;!P) z5&ea?_Hzmvba{I1R)3Ve+$uJ|>YKKGv)TEs`Z~1ensJI)0W+X zfp}`afq1(5h~VRXfcS|sW1SeSNBTnXBj4nqaS-o9(O2odB3!PeRxrgg->B^uBtXE~ z9wERb{I;|P$788ort_W^u4t~?Mup7PJX|$0Oz@9~xQ*QmiNsQO4A6-n3nhoJ*B4tv z5hK(g3N}cxNV_&v3FxTVQmLlgXARe#q&IuT(PMxj|0-Z+K*F&%*C7|_ak=)*#{1=e zJ_77oJQ}JdVjj3dSWj-oDJ zkRqBBFfcIiIASkJ+sRSXA*%P=FJ`U74etPm9|#f(Exi&+ zd#&Pnk3t6MlTun}3@)6L6H*wCzIcU>5K3d17SmIo@Xu-p_agd?O9g8@FO8sp-2wFH z{383mjC~*KsYg%wpm6j>T2GtGOl3qAbO-?ky88g%Z?l(wra9yQj3^$!OEktetsCZ8 zPe_Jj>M2OVnXkg*6ok5taG?#JilSi{$Vz!UTAY<(5CPnnb;OC(SK@>Wg>mZBYpW33_M>O*U(izV+%kK$o?&)d=1(E zy!$BMYG_DM;J7{L%4@`J(`VJ$&B(~8|Ij&$Dqq7S}dE8nS@u=kn*jY6>T z%MC6VRUg^PDg{9H0<<2}yiW>6PuP{FhcOP&MU&c%@9)lbf1T=;Die12@OUBWms~)j zOS~FV0gV=~&ZZ!Krs%PV10C+!`&{yBQI%wb=&I@MMMm@JEWuRg%Ul;l5K<`c*~9~2 z!f+}YXi@cE`x9CTx|s&g1?APBFqnT9QO0$ODt5PT_>9cQ)6&Kcf=*&4P!wa(7x}Uf zaLVrAS=ZuwYf~TAuVg3`-Me1WskC5&q$5BMr9+_%G!glSWMo|v4Xpzy?q6JO3kVL3 z5~c2$n40=5*yzyI2Ci3njZn({gFq`>lP796$U;N6#xA|9x0zu5c3538Ln&v@gV!)+ zpi|<}J;I&CprgbhFc8Vl&F>f|)HJc2cPLgU5)$==Bl^8|ykxYZKq{nL7^0VnLRr*c zvoH&%6H=?Y6g)FiPXwv0SMy#>}YKDei!t3 ze+dBgN$;&hUIvN^MeeWtu&jvpz9_qz5)Tm{)xvS6b_f_{hp}IT+18-&Qplqf?UCXZ z8676WF5W^oUn1@B(V;ss6eEEU%fZ9Zik?d$xfAKvZ&jYZa-+1*Qm_E+PalbLVoBS?|HIQNOz?(Tafup1PIUwFIgjs>9sb|Age1kd1 zHcggxcT+`R;hm8`cw%QalzQZ$IkxhRLIOz%-@y4@@`Yd#tx~dA8^~~#Rw%CurjQdx z~~v&|Bk5)|B2*d-m)ZVXwgUnnZ`fHsLE$FxXWo+CR z%aM!aWX=K59Ttn$Fq6JjH~na&S*H&sJ|Md#2w&_nZ|%l-^Y4QswUQ3sM#2Ov-s!+uBgXPJw_47YXNI9gh|4yJ)Ei@a&Sz&J;b^O(Z zv%JMa*;4qJvW4ty=N#|sJcKw2RLfOx*J69{OzzW7;;(PJ=Dxpq-Na8$4r<(`eK$>g+gDGXcO0JP zUw%ov+3%|$vF1oY+K3vnQU9_D;@Fqu-(I_v?97vieq<}DJPz^DQSP6~Y7rPBQBYEp zr}Y{+0~!8~)3}cdF9yk{&Afa2eDiaiR2>=olDbz=(`IMXwX(BSGCpVY2X*3UOhgf$ zbm~ADNl|wr&3$1672Nf`Bl{$7c@$xAp(>-x?l<8FG8djdAlj*V@pbpq-MVYp)eXA@ zI31OLR1Fr4${ntmnoFLZ`hNa3C3e$OG+96%tGwnY+Wqg(#}1#3rrpN@7auZM+KwvN zr13hH42^8#{r|>Zt2~w=R%5%_u+C&8c{ZS&j_Q9Aif!g_dG`E??CJS&?dSb^v9|T5 z#2YrY5plaW32VO`Ozw;DC2JL^r_%!O1YHFb*A#Y`l1VAT>zV`z*flgUMfa<-{He1E zX}hclodh7=i!!b5!51jb2rtVDQJSI758omiamt5n2u;Q;s(1{>5i|8@zkBTKIid0D zZ@%o-g!sU7WlsBjql&>nem_rgfQe{zKv^c1qX6CQotXdX1~U_uLB4c8HJ5k0_TXhA z-SopA0oFdWkrF~LTi-{5%qKM}LTEDw8x>|bvN`+yEa1A18vZ-2qg0?+rORqFZh657 zMGJ*`vC;mfU=E@8aTr#UeDP)SHsNa))KBQn@1oxuZF@L3r}D!gU##rFAg1<{DdWej zdqOI0{}G>~!KV%8VC)2l{tc=_#_)Efk)Q|&M#224AH0(n6Zd~vfJdz>vAp2*r@pZf ziDV7i!twu}4BOq^4Y}D2`L92?wx$NR(?dLPTf^KRgcbG{T0c^*9k`nZ1vO2+CyHni zA#SpBn2+`z=~!r4BN5NwxffYLGPDPQGYLp3J~XjzNJyHk7wthZWQHeV`;z` zrV2U>2+-;)HipN<;EGGy*+uQBvCKyAgda)rmQCr~)YQsf606tQr2kSH8@^tcG!B)_ zMUrMywVV*WPPghB$-#=!n7kg8?~vi#U;p-16c)RlhtvD) zPTkuV4>r@h$N61n=!CxylOnvXvZ}?e%{aP-4DZFg82h>y6N_&bYXeKbWX8ljL2IMD zYcPy6{+}lB9IuCL(lLhjdKqS1Uzd~8e~6#&-fw3jQ=oxD2y`}w7@#emPDTh-%%z9h z$R~wfdU5+axE?Y6PNX{_4DM7dExT@Kg~=hOOzh7eL+VI`Xd|Jx)CBf43f$K5hElvK zp+$E9;B9|#UIJsAl*OYnV59_=RO4Ew$CG7P_I~D31;#vlLxc^O+fKa+*$ip7m^lr1 zbzc4F68?y_*_?!!0a~XqW99AC(XK8VtWQf5pu%a+D;bqNRL;_&Ea5mZWhn5W^3ltG zT88O)7lf?_?$CkeI~;&Rd>+#RQg=ds*3j;RDxVd-etfeWXnVR$EevUr}i-BD7mB>q3Y>RpF?6q>&2NVUH<%?x+JbQn^xOV&Uv9s|P-_soL(So6b9sc1G zW2)EMkY>Klv$Ia-tHSj!bCS8*OZ~^$oJN($4e4K8d6m};=k-x>2oCL$1S948B8%i6 z9GR;(2L!UOcO^x5BwStWY%L~Vs^Eu~9%=@=K9YGFKRx~MzUM;B%WcI#`yAuN2Dvn+ z?E1lvN`O-1zV^K#=FrQx2}YCXzh7w^U-IzA7O`FUw?F!tU&+?eW>R{v0>SmR*^t~! z8?SOq`<}}hLmXq*fMIh7fU%r_eY%>V+<((^)!@`*?w>x}oZocT3fFwR@h3g(iP-o- z_rj+l7=@;w$VwSakpM${7FXp9SGOG9ZxR^8cxY&6xxIFa-#~nF=$-HoM--|?XE)$0 z?*=V8KF{GH1eV%?`183fpuOvg*5ZbF-!@o&OH6CY-ZW|Lsm}C=?4}r1!}OqrGwKQR zJ^B*addvSWeYf&_bHJwqbU3)flCAogDcjb@#X7c6nvVYA`7YsCoOh>ab?I?Sut~e`~KY{Qh zx-ngNf5YlixjXCQ)dzt~?bq~_-$`;14Tac3hCT!3aD3$}DpVkjy3*|F^w)v(oys z*6gLjXxQ}?2T~dFK2$d;@V4T?LBKMv$)on$=Qm@v&)fJm=k?)sGG1eOA?I;&0S?n! zgXWjtCPs7QS=WrL`0(gWRf`}R5!n;cZ3l7Yrum&(V%I&jH|jJipBh+J=@~EV(#G-pmrR^h6L}Z~i?K2Dovr+UNmLlu!#5^fFAhWx&l` zGaCNhIfQGmoHS+?-{rv$G30-Q!U|+iV&h z)>16@R_K|Wgm>C-g$lDiW?%6zzzq$>*uoLKb>-F@Oah3E=TTeyuZ93HrYE zD^=kPo!6}8mT00G6w0ZOvi+nahp1fWdfQ+RSOf(H2{42j5_3=xNr)AfC{2F$auX@> z7iY!=FO&fa1--q!MZ|>`@VWWP{0~qp354G-Xi2abetv!^9=>8D4qBc|6zPCv1@(ahvBmzO2}7%mIk#4FC|lrpVcPif_M^8s$8(E^F98EnsSI8J zfw#{yXWXx++y##vJ|U2xSJssH*WIOHV(8MJC^1GWLVXk+Jc3V|jD`UU+EF0YDpbQ> zC@pl-4?I^iaWc<~K!j--Ypn~I(1jK^W5{AW#zD#Wx|t$V!m&jt;^i!A_%40*1;Ov$ zB6JO;+y4l$&zjDOmE2QVb#aqJ34=4mwK z*+|arqSchsqlF2skyU@0h?Nfsf&w3rADsPRt;U>qfMmLpg;T2VT=iDP98pUNCnJn75cfoDfo_H(J36%fTYuD(r4{l1V#>b=Q>BY^Te&qmYH(_cQQ-f=vz zzL!!}>?lL2UfHPan#Cnjgsp(CJTp+Hx#V9=M1lV@SR)a=9hVAXT->Ze#Z5vC83bej z*{!;4*tx6>H5&jH*}$LSo3VpQD3MVt$v}EW#vKK9p~}i=Hor)8Uci1cL!4<`YiEBb z)Y8?p8k@CS(^^xrYyFiDa5Ht4gR`O)juq<|H1S zPDe*ir!s>2kviPTfGbFOcChHo4Tj>{RV$?3$2vLH=S%9ph2Si&VJ3k0jRLUnPxfLX zjJcM)Wr}?cTRGG;90=m#FHFnsKO2@Y{dMULAXMt5i({W4(c82ws78PYXB_c`Gn4WbsG5)421xbm{}wn-$Sg} z;f~`p&`fsrn9{Vy&QI%-N&gQiX$#(zIx54Rh=FuKp)W~*ofi3G zE}>Suu`F=ycqtg1oXht_tn8|7qf#>a$^IKWMMr~gNPTZMJO0Mrbqo30eQ?vY(!700 z&w6Wq(<|5d*Zg00W$mkr+v6Ci(VE(+x8PoZA>Emh?P}-xEEq%ms3ajk_IMzAG3W|4 z{rSfGEbwsqAjrGCOwN1L{NG5&)i@tzDT)rTf>c|XKF%n*``~ur;NJoLV~WKyxZH`m z+(oMWliD0Ji2#eUnlI6(idhTCL?M4vZl^<%gx!S66kT2r8ff8QKf?r)akI$eC;M~B z!qEl=s>}@9GEcfQ#|~_xMsi%|DFsXtUGol+&N&bil{OtjEUascT7G zu9fYdKVb8}RbVWrNMLfsYKQmXYVMzd6D$4c!p)`g?Y8^P#N_16PBWb7^tZVQAI`XM z`{f6@>xO{mc8s5ZZh%XTP!I`wZR0>)O|brps3YKOS;u+US^Hu7oLflmT{rV316~ZB zilQ=@|9<_8`l&%onEbo2Qb#~SWu(9WeCOCgsPsy>*um;-1v9_XwOrqj-OZ58QuCV= z%Im$WCpB)*)>p>I7fUvu=3KV@mm+%IYo3I!#4Xl9)5{JMX~0h=k0y9R_OLUJ$KHmW zTlY1&uohuFMBG?m0!%SGYf(IC{dfO4OY=VxfyPi03qqSeL%tnHb%{ZHuN%HIkVjuG zM|_n%;<+g?51t-$`?UShyzY_l-ohW4T{_oaZST|+Rr1|qqzq9=Xv8BLckZF^V{&V=1O%AlCg7>iOK2CI( zbr0n5d!b@&&~InZv7iOUZB>Hr9@BweuO>jlp6u<#=n_c5Jq7ztHa1GdYOo`A4Mp6n zA^#4q4mxgQd22p_k>`qF7=7@?Sh0%fHmaae?2rKI;>NM~v{amjFl_VJ~46;Kc6Y#RZ5cihwY6;|CANbv&{poKMCE0N*AvLgZoCl|(oOs>+r*W3C;*=VKfv zfnSRynX(o<_K3J!Z|?{rf~i)pl943au=CspNW+l#mFbe}(6Qf0wBY=re->G$qiMi66r$g&i< zQB?4|s))`h>O|?ZN%}Cl)vXoR)^*arClZ{y$?vLNUP~yuWtl|1Z)zS;RY=qInjIZ7 znJ>l)Dih%OeC{jr0E4Bq%M}VB&Hd?jRN1AxBq_W7Kf5%o^+{oWI>Z_mlU5DhSsGT| zx=YtQQQDvWQ3?RC_Q)+FXZxjh#;ZU3o5{_cuij+eS_aSFkblgP*mAnsy~31`pqKr5 z0d^at^&6S|ci8HM>Xs(pDv%E7+}ozZPZ+pfivN+q*usaG1Rd};;(Ulpt*A~lOf#gv z@l<6Ii)2qhN5LdCzR>8^Xh9{)AO1R}zL>&e%4W21=KR7uB&;C{$Xa^wd zNbv2x|2k@iy5OCe&!_z4aUA;opdMpO3rq6-li(> zy6K^oi4m$e$uwb@)W0MG+_AW!nRVQ;X}Te*6da}TK)SACB*5?~DfpZ-BJ5?{Lngm_ zrNpYDAEpL%3bd&E*<-^XTAcvNqZATvKoKf}jr)T)?zHH9MICv6V=;7o+}LoXUg`DXYbafxq(}2Oid657vJAHPw1>5hu8- zmni-BhVtecQSkQl!6in_#DDwOA@_#eYT-fP)l<2vvWXn-G(qs z*IZ?-g#TY|&2QkqsASQXF!}ZXTzl_qXgU@|k@*R3U4A14@xq(9(fOd!kPBxv@ynDi z;J~46mkR3R8XFzNk>NuhoC?$z=`rJ60QX z*iN)BM;H-g`6(00p!LpjV_sMSBCZDgu39({;yK#(wd1V+Y1?(eKsnizyZ>Zt>`H`rrI$$HcP-UvG{oM_F&>i3{sHBM!sxh|K>NQ|B2C zS0AtD z{XO@y*0bJc*7-OeaE|}k``Xw3U9JbWlbg?NKzr!f)xp)_HJS+L+`V^Plv#`;o)-Tvr2}=H~W+H{vmVFRYF( zPUm^QHvE~q0T9VoN*Q|$D8t>_Kd9BG}>wjkMFMhBKi0xQNzXI#MQ*r*2A*8B_=?^|CDfb($IXi zsuSqr={ejww_Yz`87*w;x7)rQaMUn;Hhpwg;-$GLlebtQLVssvzC7TdnDt^smDOh@ zr^{M@6wC|Ed&~777%GR_f5l|mhZ}HobesewA=ay7@Xj@=nvkF}>{009agTWC2qZ%z zm(vZnPw0}&%KW82$TVh*|5yxZsp6^jXwMCVEi@rqrY8x*C#_(YK1Qc~Bxa z2%gIuZ8|?K>3t!p*oIG4q+bPj7^WB*Py2fvwGOTk*fQUf1Kv-oU%K<-{FbHZURYUi z(7G;e;9++I4^OQ5^0oSWk2am%Ufm0=5&=uK1dOnUyQHV!hMr!ST0_#S1UqsC{b$C; zBm)C4ado}(OuSgEZ$5oH5=EEvs>r^ZNs%FrUN|i7>T;Xoug5cX5b)ACMEGG&4tUL6 zEc1R+z%uVFM9?c{M+wvysw3j4ZI&2XU(3YC3pSjS1106jM9m zZEU`Tx5su)%tREnhEqp%{&?MGb^S{IaGJm4{l3&K>7FaqHj{stCzz4n=ytfTMFOpC zUK^uErMhdOUBYi9Etc^`Pz9^?(~wz41}GaLYKG)H12GwYix5QqSqnck9VO8SCBMQA zQR;lV=u~((Q)FK1)W)a7?(U1}JFu(3zTMUc?41u&>Fo@PWa-ypR4^lr73aGyyBT}_ zSG^LCJ<4{iN;3x?Aw zz}A*c{F>-8V8_G{uNjYvrs@3gsFS0H)O@Y6+~Dh*azR!XH(2M7Io^PIm2ahu^{}3Q z#x?GCq{08qe&^6NE`5SYdC!(>T`>B(vA}%!Dv+$)I^t*Yc# zM0bEJE4JK&lOmi+bAasfr2UOwA0GYuajSxeO+fJrB#&@dz7e@PFvx)}U1uUyCW&@7 zZg$3mUsFZVrr`b_j?e*liR6D0Nv%fxhKDF3J?ip^+$BF z&w|LcI{>)YB-9$wSiTfIMdmO zayKlc+45SJ_-%F9tzV5S%U#6DQDo~d7$fsLTGrpP2K=IF-!NmAj!Ce38+O|C+zT~0 zHpU2~3nptCls^yBcI@`WE+2f0oSDeM^TBC^-+XyWkZAFfQu>lhiD85RjVf|3PQ65P zV`E0qu5wxIxkthHRFU?J3kS^c<&i(~wsxPPQyM79mg9qH6(!8RLIx68Gi5i;ObO*N znM5%9C5mFWD@OlEQX|%L6`Gy>m2Z%U35%ez^|&KICeTj;UbFBLtILE2UKAnHeZzTa2&@zc8Q z%Gsc*F-oKoPYYHuyWQ7|6=JUa0#l5hDn0mPV~W0wB=gP%hb|V4lgtdTqeeAY!>jR1&Q}|#ann``04s*=j3A3(k6%H%$>tyL@=5P!|l2+PJysCIAsx{wGSaX2W zQX)U7KM0T*$~*oLr+@Q~{B6V|yT{9a-`crKTG<=I_8pv<*5x|34`|a_v23X_i15ZY&1vma?dA5 zF!U}eQP~CqayHvAQ>Po@{yAZYN1%NFAa&BUw~l`O;2GMm*d3U~68sL<&%6#O)_G_?51=kLkCjUhtWhV!ifaCxx15~W80 ziqbd0x1%EYXt|kcmDjZQwc#?;Dbk?QQWp4WtNigthMx7&!}u4z6{LCPfmgoR*GJQ` z$U6Tse{+wTXT&YwZ|odLVlZ0GgvG+jl03y_C1q$@1_vL*DHsa5!xz(g^JdN2s>TS9 zKO@8VDC&+bAxIG=Yq+`Y##g$M)M86TO34j zHZT~(1^zSegxgH1ZPCpMBk*qt;itj-=}SYyc1A%tJU3x$g>n0Ty#NVTKgt+-@6^~a z&B&x@WNdD4KP+nW-pj}O@48%t;QuoeiMVT&a;Y;qdU+Y5E*Eqz^LQd>E;nZ~HMzLC zxh0nqoeHHZsCHi@-1q5cGow-fT4rbRgmzkecN?O6YX7^MCv{0GogYBK+d>6Uq|icQ zSU-$rT@BBi2e?uJa_M(x&a$q)mmbEC_IgdrBpO4V{M@h+YcfP~nDGG~+BxW2^rCWlSUMvRGp1R}yoOv9$6 zB(y~*t*l#fv%C*Vd7X+jhaK}SB9r4qNRa|k7Z5R5KoGvWvA2=oc$(>_r@nSjA1EAH zKmfUm)&7w9oJWeXA9sW!Z7<8nDNw)w4Gu#&QuT8{4JJ|vCm{`?&Dr(2t?;{yA_iz_ zXYoN$D-#ouvXR!v`G(Hts)z4Pis*3wx`qNSA>Cx;8khPvtb-UMkRPuH@sSXFM3Nan z0LHh-TwZ|uv!Vf~h)rSR^6TVVoT6OBWDwo&*|X@X7Oo(^re%fQQYaan5D;&i>{@Ij z7_e~*+%;~#s-oU%-fceDxq9KBgyMW&)6#{MoUW}onmK=SWlyuidf9q)xwC3#AOH6Y zSs(fLL^kC|BzPgAH;oa6hFeVAy#faf0`u3db%TS4aDdSXk&!!a@%PmI>cAy{+m!h2 z=cgp=J(!EPjIfB7+r%U5XvQ}jq{@1EON=oGy%Ezx3$_(GN+GY_PXqNY7ZD!%FX z{k`?oWk$TJruS%jCh*@vS65dAVOZ=8M! zwJXN_LGCaY8XPRXx`gQFMpH|5XDFU$LoXtZO3q4ZgEjnlegP_Xe2EFQ;LC92gBEi5AaleCwU z#7*66<^Cj^O{gA%Ry!UbeBk6x*prRybJz>z}t*-%y@D{}Wkk*tZh+RBgXPbgl9j5;k z`ns=H{sWPSboHTt(@T`tZx%P6exI{}JkwZHkVGEi&s}*PtZu zE@Aw0^@hAS(Tem@M!K}B&yPa9V~&F@B}EIREOS4wpjcp}WoH%KN|4)u!8*ZQ;xvXE zaPpiN;|u%`&B=P_IiYYGCyYs`t&HR;4lIkN#|V^DJwMqx)YPZ|fU?T0ua1u!mFP~v zesjMl_K8RdZ_}%mFx6k>ymuUu0@`45&%Fm`YA0|Weat{hG-ZyugP1$)0p?W`8#eWN zJcLK`qzjB4NdM&Y{GlNG8h=|jcT{7(7Wv|@@)mLk@SV2a)fr$R{jNDs&jCGA3-2}N zBtrE+>&!mfbdgQ4vKkZ-rH38lB1M7<`Va5l&D^LhJvR+IVDZh3exwwuLjp_*FyGco z-trtDcwy3pajCvzQ;lb!D%GF^Gm~i%OB!`6C+4NiNl5ttYHs&+)M;N z@n;_;V1xDT351~`$l}1`yT4od^vqSDVjPejNyFixpZ5?8=~2+NA=Fa!QX)-wHiaaw z#SW+Fp`_5b8eXNUPy0O%@(xNGL?!7#Wz_YFnz0{sh!ul}{GW1i z&cCa>e4CRNy`$c$Xa0e`oo1~DA-SF++*+@0y>&dyMUnFUwB3Nf% zm{Mpo3P6cDa&ZNOU|lOCA4nv3-vuq0vRLAa4~8bBGMZzD&O+-#c1XPUOqgh z5C%c z*r!pT=eJi-YYJ2U)l0@?*Z;Y8m7?SVHIg7^V`E}ACERaSAF*)vgXlj;}m)lXn2fp(Eq|gN0*9A(m(S3fC{V$@>5HR@C3Y5Wmxc zsa#o#$T<{btc@j$UN+eFQNHHqhn|vL-w)qhETsWRhymt4wRbWRwk;Wkg~E;`j;He; zUuRElGsjK_GAL-NGifo*Gg*>X#rZ9$(tWzvD?gtb>$<+CNy(ekl2nNuNSGhwR_%E{ zTE4MuDk&>LYxRUTsOuWBLI@HlG41=eCH0}Ft;^;-JbLz1)ZWQC_*oq+fQQ7trm2N#v%kWWcO$-x8wLx1H3$Sp--638hC{mt>Mh~pfQL8%B@7=Kc{wqfMfY+WFYaS%cIlNpjto{l(?H|v zoguSz%bE8h|6T~n#9@0}F1+u1%VpWddpajruTT=BzB^BUQx9JEA=NCtt@7`m>fz&K z-`cBZZo0!hM#c8Xt6xLwyS;fj-wtMMV=MNH*R~6lZkq&OEC*~Y1GC2q1g*OD`lhqK z3-@?F6d)@psqkg$;(n)Y`wz`j42$AZ{v~C`GLspdB8!e1Cl(PoS=nC6z@r9`Bd1}) zBCGe#5k;;-tiICyp)q~p`Pa-_uzuLcLANGX0wXLVD+_LE@9=nY$7^+Mx0GJUH6vzk z&mBHKGh;YhpsJ&NGz`kDq`UUN(D9`)A7yNT{MsJPs!toARrKVT& z_f)2*FxJt;S7TR%{@PbZ3d3KWOKj&#+Ab$2+JM;1YwEXfPQ4IObz*>#nO&r6W2A%y z;M~9wqdAm*x?;%9A^GfaS5 z|FfE?W~$7uVBIdOt*_Cu=vMtKBR#$F5Pb(Tex@m;x?McvkUHvFihr&wS|#nZm6v!| z#;=o*bvjg_7+p|MVCi#tEDZBM3B0lez(j?E=dnS{>0af=gMGxrSywxcct3?GnUvE_ zxT9pGq?q0*U{n_1WGDxc;BWTBMaE_7gNOoE-&VE8w~CCm6Cqr5kE4_D&hBzuVP) zgAQaniYhJGYoY#q9Ur>IUo^P$-<5eiTct`KEUc@*yEvB@+j)x-jv26crNvupk4B?} z(s~K|j@|iw$}2SD`Sfp7!noZmEdD`yC{cHm`H34U ztLCS<@jkEU-1>~31GNDc_4tMKsv%*O(H+sMdZTN6wtEp2OMa;dhsASTyzBkrk2x~6 z3`jQ@?i6KdKlK0DfSHg;@@|^glO4Ht@Ui>>_GuC+aBRF_QikUPQu_FSmjUnXRa=>| z?Y9wAlw>m9;@&y|aU^d%|Dl4%K0kh}5-vh zY;y2U_eZ!B*3yC5on0$EDgyvHO99-KmlgUh7}Gm`<@wom$o1R z2>PRXeH7 z0X*-{200PWOP_@3(kf5XfUAN2=&9*xi>1v{Qb4BG*AN|d^DZ%f=wO%$Toy-M4YQu< zq0+Sr;6H*e;>BD9X7Y)e0e&u@JXQg`XQ~oSPO9O@a7KYs$%cJ~0Is)?E z7W~kNJx95tl0XeN51%spSLaFn9 zUu`1~d@VN})6sH&Ra3HF zle#ga6{dS`GxO8t{7O{Kr`0qt;S?U&8*-T>KKLH=EPq)(lK$bSX~9zAM*>jDWv zY_a5k=Rs=!a;&kVi;G!vz?2rgzpoGcIS<~vAyy^i99>M$%v?oe9Y@3k?0;0cI0Nob z4JY4?H3A#Q`>dBc;TECqh1>^4L9dPf@lHD?}jQK}Dlp`|4?h>epm%@37h+GyA zx_aa#k^R1YQ^*}=IVftKDaL!zsa#$z@{Rd)_b2((G45aC>2SF%@HvdoeXJ|X3>wj2 z&>6~jNne8tC8Xc6*6rmLGa83RFerN1Vq;uX`-fI_9~kCyMFV-iZ`%_?^?)1XdZbWq zs5|1;Mq{3__|I+Oec|kPtsHxt-ADz7*FeAallA6PkVi9UBCF5hKC|&-i7b%jB!}Y((8%1v)coB_vt}_? z!Fd5GB%w7X+Lu0BhAfPo3~;86NU*2OXwo$(7cu;` z@k8<1%a`vpthd%bj-B3>b7pPs1KHY(!>g!E-NI$Zb_QuF1M1A1_l74M(7sg1LeDOU zGhKJxwkr86j%x4j9e(rw)|zO8%ex1^CJPfLd2$=hMs}5Fd$XQsx@jbU!A#N?v$ib7 zy>C6d5n5pJ=QQ-!ow%P0N?9tO{=)x0o_RNUaCyCv=X__cVL1UZaMx;3+=#8tu z{@NX#*3i|s)}Ji0>oL2TGgE{FS)SFv(ab$a#tiZyhos2%8;{pbt!@|$pNVWGo`^B1 z#%c2jq>(b)yNSg2=B;lIc6W9>R$^Gs<4nipCs*I4{u^s;2=;om>CahCdi#Os1^B`K<#Yh1O58)NU5q3^Ut1-^jAFPM-y!tW^wY8IH1YwgPI0j{Nd> zHuxmZ{Tb?%_HGs^d01M1O}V*=;7^_I3y8Nda@*FKe`E7HMB-mu-LD5fsg5&Ub29wr zFl6Wh3pq}v@Jny@Dlrc-;HB^3-1FAg%uBfzM!Qz0A##&$f;LSFLilgsXuUG{zXQiGKPr1E&VEbXRzoI1oiSCt* zSjg$8zQaB8MB4HuqWza$c!A89JtQBKep>C=^gF?bfJkV=)AG@> z`(n(Ds+3_hPS>m$CSSI{d`c4Tz~D3Vk_1KQGx(^Tr+57#dY-pznHB@HcoixB7TVHA z&>wWl#MPOD4+|_g-{h?tsO+l~#y1)iL|_k{As!=-CTcPs(~|_o^-7-cP)ssz-XtpL2u_5zdCWUq|Lss+0|_bQfbfX`^yQT9JBHizUiD z=nEs!m&bXslvT!s;kSCIX30pmLy5|z~mi4MDtNzt|eaO8*hBt4t1)Gll z2A)J+wOi`>@1S%)twk3W54(o+l2e`{kH}@q#9l4J1#Hf9vey*;aUB5--4r{xdv5L z)t^6q;=7{-!gP}sOThv<@bYBL@ztD%tbR-DSR=*3c(ntFn>#NqZ}M?-`zuuDu$P{h zS zL9>do_C2wELRT9>t=-80-Iy;{-mhJZA0Hb71Ktptm*hm1xGuFj))MB?u3lcF1ZZ_x zavZDtlhDO~L*yXWapdqX7}-OsG{wci&wUH!w|THl^+(lYbj*0tEUm2a1GHiTKgt2? z#XLtDhhee826(9ze@N}TE4|W^-D|o4WT0?t@X*V~BnCxtS}PJ=R3RRIpC%ahVeq}u zC>j%m!-a}4;Bcv{mcspG8PoCD~)j9Dq$os6G8{XJoG87jU$r=7_DWs`{9u z6Wgz^apvm_GTgxIT`;aQ3cF_?BFi;_7Uqz;o9wQ=&eb82)qhQKc&zI;$*?9Wb%^L) zw^8xx$_kd~u8gb*N&iqA1Ci@s&8!gz!;rk6G7LakF4 z(<9Y_R%&UOB0AGtzJ0=DrvKeSO^!`7o$lD9m(w95a4dgKJKzpD&x~ljfoc9 z;^A(+yGnL#w+%yZWUL%vg;TbcoIq{mA4RVe-60i9lm?<_;<+JqrDOOG_O;VC6U=0) zaXe$Hgg+lsV=C}i-}XJoTl+k8^NRa)-Vqd(=^DU2PJo9Wx6(sD?bIl1e%U^D-2$IQaK>_e3hPPJ> zKOz@7kTOrZ)hHb^Cs_*pv-cE@#^%|JqiQaD4-rR5`?VUo)Xhy6xRk`}AA~+PY3b)wD>Zw9*EpvLfJ{{L_uij_jx#bu zjXu^_8_eal$dvx6y`q)bf78m2b;dqxzdXO1e}2hLGp4JK{SG^+=SW@@2S zYwPJPNcJS&&#;vcq-?#Y4E-tgt6=0_-4wHi-+oUcM&4HzM)W~ zMk2COasfWC)Ai+~O~j12hQE~!9(w$wEVlna2 zND&ztHI@#$EC0HaGRdo0WTco*J=vDq{-jl~aZx{2rs+3l}NH}I*?bk>M1u@#iG5J?+Z2?<(t`O8r`-c-4>v1SMWwnt8B zTTK_wNH|*5`RSrE7=L{K4uNZPyR&_B<+wHQCOj@;<@|h-psk~W_DtyoX3rgOTkYfg zs_|$s8%R)P3eOy&QSZ{#(J0azqPBAK&^s7aDnMI(6pDaT{t#a z5Z;l&SuXh0G->W0FK?O`x1acZzyDPr|3`!*DJkTdf0TyU2b}@+k9AY zb=Z6sqk5zb_C{KiGG>lbwcZYA8oF-{%`TR<&-=DRpJry25~E|RTZoX5#hHK~;B@dR z8`gm?#FA&19mVTEf=F|4hya~$q}nZ{~IL; z2Vh^E1R~9I|M)8ACq5}2-g$OotHuOvSZ=ymMM|Gj@2a2u4$hh-H+qyIU+hxN^EI^BSF{56kK>WLTmwvK+yBFH!9T%~##x z%g7DI=!Hlr44x57^gbw(PC;jOsZAJp8Z+?OXh+cX#ka56~oM`%XOL(Fq zwgb}ph38FYKKzbP_}u~`2~N(&`x`HUTC7Fvk{s4aM+}D3WO0BlomFgPiY66rvBDE# zdu5VNwc%gbws(YR=q4k>zN~^h;wV#it>r%Y-b2BdrY#1V_#ieWtGPc(Q3^0UJ%gLo z1&M6V9Z|8C65CHLItY_@YtC@jC`UXmLd45czdki4=h&C{cxjG_hV_Jg!G{R*GPjDl^im2gZU{rmZy>?-c4lKw20u+ zmIGzg<-NxyidZe>@387k5ST3f6h56cRnF|JI%9^YztgI;T{k5o#!dT2xQ)csj;t^!qj= zpVh|e6m*A)>+zr32Gf+`pHs(EW~@52mqXvi!qt6S;(w~%xeD}?%96a$*BWqPgk2{R z32VviqC}H|F(+BA8D=XR_gw_ODnyblr)yQ7twK&YTiIV%SGx;)KGytYGQ2ddDd@)U zmvncdElgD$NBE}ZH;biIWA?(_PP;4NOz8>_A>DfoPBdn)D1Jo0uIer#$&diMG&#boxPGG^@OO&Ekh7?8LJ z3%hP_K3;@uW)6jdq|^0kY-kIyBv+WwmsSZI3DGI&}LoNM2@pGDThAz zRih{_c5vgN?EiWJPz*H!^nb1g?BqLHk+*W(^$ZYM|Dt*k7qBS$uXFd1Iw#q1YuSAx z;=No9NA7z3;ipq=3j@UC6*VDnJh;BJ+OVl++j*k5Eg56> z{pV?hANmo~21p>^ zNkEOtTIDwMHq3y7ScpL!p^1^a*IwFH@`2y!y>kqP^`p~-2pJkl5s3nfs49i&{j;o5 zNZNO?@s32lj_%oJD_+H#Vdy0|;C#){POZV$@;Vfr+PwFc{?uqi-+Nb7&m&pwG^*q0 zi*pw@JNoVy#+W((I+mrsW83sVU|!_^T#CnZWm?x$x^*##-`|YIKZKytMI6Bd_`TRf!yrY@yzg{bG9Od&JH~)DglXL74Lz$IkkX z31#xR(SXfS&SpYys z9%wl{tm%2_{h)kZATM!b-Lrt?!13@c7AV9YN12`fEx$S~_pLa5n~U%hhr!`^Z`%qg z;1l{+RK$s|xdv6xx}F8_#dN8-%mGoPD7peIV}Qd9<|6Uh_(<$h0x!(2+mepU5fjal zWZl@Ev~E0Fq|Unv8kU%|SiCdXgnE~6QGBbePSBd*$EUifv^6$@vDuCyp3#49yc;fY zT1!-RV=`5}m^xnD0^3`^$5=hI6+gsW&FaPSGNtl&I86|xrY;Z-4dzi%X+u<{S7~S% z4OiCGo0e03V2r>YYbW!H>5M!=7ve#$Qc*d zKsFDaU2J8Qj@7>}^yL_1$oEG`#n@@$2y9FzkDJ|!n>vzqvi5ne#!WKaFg_BRM*{U7 z(S~$jPg6%dCDa2pFz|OZFrXGR6;Thg47?TFYC5Pn#A}VDF=c zuZ4#Lv)y+JRJj4r(6#$)r$AO7?fc zFsmo*OYN3(W2)<8!CN~E8!EX$ED6Bxtujun^$)HoE?N;hZk>|lI(Pm=FUUx3J5w@)@yqz67JcEYAY>$i~Gby zW%by%lp6pzNDK`Ayxx-~-sOO2Ghx@byx(L{`6jcm!yjVegnjhHV6LOth4$+Qsh|kJ zdrfCUGBD}`5@uNZN#h!oJMyN5R7UEU;}e{bhVxz-W@+_#R{ZB;>Tw=H+2TR*K7i`s zVy#SF5lytv>CHqJhwZNE379$~@Veq@3XWq*YY-Z*bHlda5o+Q6?ilCbuZf5L3=c!O zt2Luh8%GWpyph)MH}n*R95Hf&>*mv>dmtGrm|5A4d~ScAptEpBg_1GLh7E~)1p=+;FmqeisceFA9VrB8~Xx-BUUpI*5xQovKQv9!uIM8);@e(f!a^hlDk zux6Mxyc$GH!76T{hBQ$XJW$s<p!zmA-~2$3m`s{`x_EubuC*KX#;S1Pg&iSTt3Efwd)~MG!D0 zk5cot!RB=1HQwc~GffkC8J}Yk+&9B)g=kk7-*9ZwvTcu_et9xaYHAo`RzrZlep_7N z`MmH>f&7IXa1l@4=*Y!R67R8d9Fa0@#_u=zfqlBpAJI`OYvy63nGgEIckHVTX|^^u zg%MUQy6Nc|cYbxv&BbSCrkA&E?5r>ZJt+=Tho!uGN96h2G?df)37t)}s}wKV)7I7k z6LnI9dys4^xt#vpD-U@03|6t{`M_D9dI+BaAFzVgzdG>I3;p=<*6?|$i@)?IOkiL$ z|BtOcxtt{%Cwy+xR$+g{xAxqIds&DA7A2x3mb&eMv8ng7=7^6^eYZW z3Fn(Ps{hz^^htBf*39GWMdlr{EU*`Bqn7|>Al}=Z>dV+zFAGl^f!}t z!1UfEEHr0!H14yv7}NDia6ERx%RKPQ6y-peWo#nqrz?sHWZCjL*AaMQj*(p%fk=a4 zY;J7oK2HA4USr3QUoYd8Y+g{KUiDF_OBbxnb%*EOgj<#m$eXKAOK76P@-24<}YJM0GS)t3ezJMnBAkIAX)F z6d5buI8Yd}Y_-PNy2Tt#nxfY7jK^XIv$0x&VFlvIglP*$&Ng}LaWdmL94v+Cp;1*1 zQzRdh56O{A>!z>AVQFQFQ3mdPiP3{ZuRNyjfO%MoP%eeR9mz2K_K~posatP_Bm_3N z%82VAK4N(a+wI7qVaLy{?FM^0DPh{AvO&d-odH)n7wqr331L|S42gsmcM^F1->U_< zm31WF(Cs$&+pTSs23?gJhpCo(IQ|j0%5Y`g`4r9$aD@*V&Ajtr`PXj$@)toWgEVDh-RP@DlwRd$&^M z(dbG!*!NZ`raVxl<*M;VGf$&<<9ItUGli;D__^Vz7if= z7W+NHox`vik*eHC+II$)d5QMK3YF9N?gxJ2#x1NbZAIJOCNb73Y)x`B)`jROOpjk) zRRk?ieTw`D;sYqWAdDPamaP#+XuHKi;Z8M2&PA;w!=6N3poqqYYDLW2#hw2&dlY9 zsUj$^rW5Sk&6En5s2k^AU=&F%hMnC!mm(@{yLd)M-Jh0A7jTS6;t;qt+V7nWnS3c0 zNA%{pGGcko+qCbMqoo=dSniqw(%v3sHPSwyGLCe%;B56LUo>qASva<_q;TW zudJv5%jirk;i_~RgbF7`XLQnF9=6B{U-@}bMKp;L{=)vf7A>mLBLt%RVA*)T4(Dw5 z()4jRsx1i1Ge+R49^2NwU714^R6)fM>M+bU zV2#m?#}d&yC4d~aay66!3QL4|s71jJC0V^NC-5Ie)H zi>`nZYlRg8>SAvyc&fE9OZ84Ebxu>g>>)3*dBThnBS~oAW0XJjT#VYB+n|pA?oJ*! z#G3?W7vcFF>H{wLa=pb)w+?0QMF_)h(iO>=3kwtg{eLAe7&db3$K*2Ch4g07<9qQ# z@chSKzdUOS%QQ{Ay3vNQQR46rp}W$fqie@NGr36`P3(4j5e9mVkNqZMT^FMQ4slfQRjEG8Lr!(Z_&Ze6PV=@%!iT&PqR2647# zc(tAGZjEXK-AUF>ZZWurSGjzk0f4 zO>?0&Lnt$ACd;l;r|r?IA^~k2*yF!v^5&TTQUN1Y7FO8-zoqR4>d0++>ocFH~ z$lz|uVsRfoa$uT{Mx-%IeXKgMAO~A&Mufsf>=SgYUi|OiAeg{r9jgb4P#mlQ@|Rf1 zdV{h@`-oJ8#s8g4GS=xrIUK}Mlne&S!<3)3e*{JTs`m}W-}o_nNlU396%PEQx1@?Zxo2fu^&qB6CHmxWjcK3TIourwH@!V!Qb!GOAq>trM#a6JDkZvV={}D58~6Hz`@3 z6G2)4$gdj~#_65LkXyr(=BQzUae53ca6yR`1^(sz)lmgK2r~zQqk{vD;_KsCJv6$i z^d!h%Da%O-B|>z>s5UdbEL@ggVjnBjwNJYT<7;fdNE#6 zq~HDdL*EpR*f071(j z7`a2n3+sQ2)G*~|8^g;e87E24_uVhQfKA}A_M*-(AR0<_w79=-fJ&7jZW+GtN2K-E z0JFREKiNZn>SWTum=@EQ_q?AMAcYgS;WP8aOvM_5vWGsG|A?=FD@>iKUQ~79HnH<1R~V(s>@Q8aFVv*Zn~~w8}v;- z!xmy;UJ)hXEb}GozNEa#Tj-M>o4#I0@#g~IiU7?VXl#|79avSs8YkHZQ3O|7@;^d9 z;C9CZ>`4bIFaWx@Be1l#RYn@Pc()PIVF{7Y{WAT5TZ{@2)r!Kd55aS5x>SR4oB!ls zMm*5c_MN)jv&VnK}d0xK?h{Xo+!z&8)oxjmvoP0HV{9}IpF-%fc^4gC#ZX^H7x;BEC1dQ5V!n6;Lc(B?FKDGBqrR6&q(NvXgNahGol>|)KZ`D|+2w9f65!*eFmrV}gHlNI`_<*U{9WK+x%IJjYEWnE7H_*3SU zdG)iYde8N~4Cz;Mihc?ou!cnL*U#@|dQbc9^}c(_p1UG;!)j;IP4Xz{YBlR}^*@># zxSvQ`0*&Pdy|u-|Yuz7OkB26wuE>WZnrY@4b&L@|UP!Cl{IhE5{+bI_g@7T~G+r8x z1YIht9I&Ta-d(O+b@{sZN?eV6sD$j^y%?=zI8m*xh`EO>?(p1l-u}<3l5M#8M$PSb zeHHQd4XW%;M^$^&anW7QZOlyJA<_QRfj6?9wk|^5HLTGEfAy%EgS~sU@}FqJHoMHZ zRTH-W4*hqbIEvY}wX#{%($Z~b0&~VvJ}uj$p-3Tb#B8N{s~N)icqYn+_g z1}rMPWGQND0%~^$FMF;P{OZe*tLvT<6#3?=ZjI zTYCw`W;~5}`xJurgm&TSZ9c1B#@Wee#PpQ$-K9-10N(*n{c+;`8H<$F}vQj!fb*b#MOkRh^ z-d>jW-?gZ-T>5@a&h*5C=UNuW8Wzmr;RgG-5cfp=@tT+jCT~zgw_vC*V~1mE4dFvl z(7NaVMdD6-7UudvNr8b5x&B}i&kUSoMMDXu{p1fMtMI{K1j8eyLFQVNu!Z)PxIz=N z$3w(khNc|1{&8JdbwnQO$;Oyjag9EksEIimdIsq+YRPod;2kHR--j|hBS3I3NaFHrf|Ld~ z;?Xi*PFDD${u-$&SH*>d782@m=H5DgnZuW;RJU1<8EN510v(gw6O=QhZ5zxIT*rc3 z)9KkE!VL7;yWb=b`sdm6%)W`Ge(!h=&s+_=REg)p5HnC4F?>b zcU{-(x@Y!`9X_o~4=x&c?i=DYH-a&&dpd#FLE)Vig_M&R+z-OnU^D&(B19Q4rO0kMt>^b<(7H6nb^2pVW&jm)N!;7~l(?Pmmtm|_} zc$3#rAqCYN`(kJ74}=e4hDJW7;i4UT#L1tVqgSAWr=E8Y&yv~ZQPIK=)v+uE71GYe zQjfJtXkF-@D#`H6;^7w3`0*~zx)fvx4$xYQyopfz{y+moGTgbXUBaG_WM&*`v(e<9 zf8XEDD?ek%;weKTWFI6$0^uqt@pD9*Y$^ZLHL9h<=`_$Xl2|91?8%zCH^x=c-LZyM z`@yJ*C3x3?Pu8Fd$uQ8O;)kY8JNr=;t_&BBBNk%f*SNc4@3_}?RgV_;Z!n3CF8jXS z{>jsW!567!hfh{>mXEL)8EJoB&={b*2bn@K>C5_2+SJG3xK{jJ}jpfUBpY z#fBCKroeLGEE)h`P;XH`djbeGwHV&3q`jm54sAA5FE6&lM9Cz)Umue}>Iq8=4)8~k zCVeEzpfg{=6sRoU{aMj8xAT7Yn!YKz%R0B8*EXDKhsQ(f! z9~d4L82p6Q@E2rcWQfZ&Uy6{R0(LRz+w0pskD@=ijs81l7sp$>ZI|w@LM*VkSL>Po z;2YWJC?E%B>0WjvhM_R0#Q0TTycHn4m6{9Slwa{7vswkD8Gy4pfNyI}gS5 z;8-iTBJ%aQ2W)>M)gRTZYhx!SB_4iNg~-(Rj7hrEmJ@6cvM;G6f&vd#c~OG)!UYg} zVSApp&^sMC92ECD*!L+<c6@-shCIVvx2L3hR!0Bl3D%L} z*vx=GAoJ2@3{-320$6aoiU`SBKmh>2^`O(lR?cixlAK8kJn5m2Mfp^X-S0lM-%U@_KS*kGlI%wwPEa zqSVxTD2h^%(U9w3s3iFU-=H0>``sx)p_O_-e-Qe#^_9L^nM5Y{;f(MD&}5SUo9v4Ulxyt_sNr=$ z57h&&Kg@#0$5&V8#?2Wi=S88n>KQsPcPjAc`8hx3c-x1UV6jqi)~u%A`?W3>lla*S z2kiGWGH3h!cA5j`+tNmI9zylOH?Cgwh2G+PBGr=z$1{X_M z;aLbV(hwpUclDp*T?VXG4va^e5 zKJDhIocy~5b5+_4wp(jZj~qW0IfC51CjjQg0S_IxyP>Pu3h>Nggk;j8`~?#a6t?qb z;=Go4{H)hXO)Ju$PYPjzYyRG063l(tjga0{q#NwYTUjt3M_v4ua2P_R>x`gE#~*%# z4sAjae?vI)WDr8cMs=6WZ4UhM2}q*bF)2JLF71^h@- z2R)tDPHF#EG7X+y^70A-1y_5q*$tQBgygf=nJ?-u#m1nSmF7%^Nf7T*@!AJKtfU({ zeMFu+#=%|wx8(V_>8<9M6#xqpky=>!jZtgdMnK!slyhqTGv~Tzr0VRv;%!%r?9`7R z3}NXq(Hiw;#haZ=%8M`)9*a&W@XZ!h*jItSp{`kX*IIU%mUX_kuHPFC4mj-7W5&gj zFsb3I;yB1S)%IE;6l7w23iS<(?^9sE7v|iH%p%97w0O=!b$Nc?=qw{|Ry;mKE~4=F zscljlyf3z&)wFLpPRRrja&sc&7r6F=H@w|I3m{)eg4?f8UH~FC02MjXc)igd8jc7# zlv2Wc#{fF45$W}R^b0Xu4U;V&(EK&lMeAlRvl-&pRPf58PDcpcgy|dayjw0{N5uR) z!)M@wZPTL<8Fhg=v;RAv{7=QbeevnHIfK4GV>3$;3nL`h{vx!kKljs1Hr(^6rN>OH z^@FjBz(i9aVbD>sSgH-t-{s78{KojAbU;1@x95vZ#eQVtJr{x*&;5nqlK9Y@0Ez}g z@ou0YDdAse=58(~^ zY?toCTe!R74mckHMfuea05;AL`zE+O>NI*_C|wHhIx=v2zJ7gibGh5khSy@SwV`dD z*h;h`a`88pLzV@4Riat8!`C$unn^xiq-zW#AbF{X?27nR17v)Uj)@QFS*gm;zizdz zhtO^2`xqEWL_lzK;c#1|NDk#Cn?v?TK2(NiqtEVAkzz^56Ev*j_&FZRMB%-#(O^)5 zMBd;VX6}p3>7{MD2P%ir`~~9N{o~GR2`-|;vFRFXNl*}y&)%9S%+I$0U%tnmtCN`n z?<>>>R`XMk&95@1kGB`Y6h#mF%+MKuU~Ov;4|S>`==xUTsX_b+3U^9=>b4BK)!{?Vz~#Q|-w{Z>8A&>@VgP7V>G&4i}-Gaw^j8 zzHR5$W@_F~qlnQ|x%W0tY2HuK++CI@75V)vXmbxEM2f?<^p(CO{WEeeMOypbOHg7o z61=`?EG;oqJwe2EiX>o*sB1vhyRk4u1y~@tB&qcBtu-u96>9jRAD5=49v8yO1=X=D zPx?(uOThN-gk_CR-optA>HOxAHd)`nD^+M4**vbCIY|dF$HM|9=GF6-N3_uzR*5?L zq7^!$mDba~5X&u-T2=x^I-OWjlX%LeO#F#E8){lP1|+|peUg(kJL#7E%Q54Mt}E;1 z5GYI; zyid+jaIv#CWf5Yg^+TPU{GQQREJDEmo*H(*_Z?Rw0p${NyArkJ1vBn0mf>S&hxI1C zOO4DaOi~+2HArBi-JS?2{LQdChq!BvGy7R%_K97LV<-7-c*@iG)}hVq1uBNV9etvi zhUv`sY(}?PjChc{Z~;x0xj!tOReSSAO^F_pgn#)nZT=}JJF7{w7;(HdP5D#E_o+2k z{SKyb;@mH!=polSndwY5l=k*YOLHJ4tWQ2UG9#vs;6#+SNWQvu%|#(jJMX1oev-Xd zN-EbvWixJ^dCMAeN7Af+iM5Xk0$wan=2y(n8{Dhyl1``ga@dA`jn2VFRNI}Xd0ygh z#c@e*`=dzV0}egksnqbLM8mP5XNsb>>4kTm8UH9YA#GZ0`L-sS|5sQSV$^?+x&mdX zb9jcD?39V!X7Szw4vT9>nN8nwecUmh0j_dFUk(|2t^on%dry9RT{Zo!hJbhZWL|m& za>i3m3G|5mmVQ)SO}AHA=V+`)Tsk*MyV28qm)mP0l?GQYBJDL-l@Up$CNesS-@Y1^vMxLiavu4Q*bm;e-N!9|BSpZ1(GR% zx_s#=Tj`OB3Ntz8a`3O%CV5t+ANP?tY2e$0vlmBN9ci!z4@vh|T@Kp?=#U#5QLlx6 zWwTuI+Xc(-%61$6*NYJWY|>ykhIl!_fwo#Q{Z9K%`SB=u6Zk;1*xN^p-?S1@CIbos z*m5XWd~?_Mx7YIyns{(2*!w{KT4zzhfrR}1{ARFE#!mvk;G;9D^D#PZ#?g6%=*|Z-R-ZOZtM30DX_l3QVRJt?5p**0 zAnrP1xw$7>-0gVW=zhfzH_-=Sr@`$QcRqZPPKaf#o@P@yqb32T@S<3WjLpN`?YCq# zVcq!ElEI^#e2jWs5;>eXd0QoJ_2_@*zU=Fr5dgIoTxIsp?x&}4Y=o}!dBO`m4Zekk zp2Giaiz>_;4@&fcAEvqGDfp|masqDTf6)!gakpo2dU|@eO=)aKa*B$t&sV>A%r@My zZpO^Dhu)s-psTVAHoP0f8Eq>-kVvQbQ=c+N_w?Qf83Ot$i__m8x)83s);pj+d_eMr z0D_r2#P00%EMe~xJ%f3luI8pMC}G^6W^jVsXbEQ@9fjsP_}4raJv`o;M7es2G_~~j zEYI4xBr=gwzKSK&?Z{;I3^0bzPf+^WtU38F42sM^@a2M~uN{ zs1r3&=g(XEPK}I=4wCezreFhT8ZO#cB^AO7Ekt;q;io?5&V>w%(`Iiz8?JX?y`2(3 z??|4UY=z!PaLCHZbsI^+7sx)o;2SBhL{F z&p1{DA!Bu^Q(xScv5U(OyO4KqXfesZ9PE z+uF0G8ba>t>jG+gII&_bXrrMm|G+F|^zr-D%N#MVhQY8PX*{{wcrehMO_mm zpVPV(S%zNJ)JIm2v$m5hc-)+L$msiKdeoy=s~Nu+CS?UKHrmjKTuh8)?z~&iT2Gp> z&qD%hzycchr{#?^GYC}=W)dBXaE+Y9#8p_9)n;X6;RO6G_%h?a51@!Njc@xOg;ecc zJ_`Q()D3+D^oH%Avv8#|T<~T0?baaV=t<}iFUQfQL#RuYWA^K%A9X{a7yY3|wYR42 zr&bI{(+=4ZHFBqO6@CF*W!aNZy^)b2c|)Fru{s}y{k39>H5LU?;<6Xe`Uam%FpCzof>+#lg8COv+eUDi1nh4(DX%FfJTxGgbL<#VCkR=LUHn z|I9%MHgUqCWhPjl$*rB4J*%e9V`B$3P%IQfe#eVEWPShGn_taXs!_D2^73Lzfc1Hq z$!^P`Zo81zwKF=S2C%AuYhIDO!x2X=nIjwf9Jg&p>DIZ2?S}Q)Sg-X_-?n7I>06<` z`r2JFy^(u)sf`!TwLH0pqh*2S;g0XgqKbDXvF^!tQc4ig4Ag4zJmoq-C+VXjTx}na z>{{QsSBypVKq9_+b86Ai>M-6(Y7Q*>%zz}S(`E*j(q%tv-hb>V=4cz#bP22Zy%`xP zPlvno(8NU#A_L#Q!FUmeL^Vt4WZjzidcuX)u)2jbjeQQ+hLZktkJgL{12fT4XXkHTrE^UmCT5G$S|uep?t|6N zflB^@*^pf_%IhV%cD7sqm=c@%Wze1mP^}HS`?l?<^{M{#e$`PvG9NVjU0l^jD?JSo zuzleNlRw?o`Q$M&lHyn^Ywulw#70JQ$7g3}sI<7f;E;t1gJ#>VbYRw$@L z0Fp5~rHoaXctD`F1_AM+iah{H%#ij%84ylF7stM*A{uO9$#o3r0oIyKzl0?S+|o&^ z1+`JJFJX~AxUP5k>Djs~@f(unGD)`0?V`pzrBz)1Hmw14dOL!F_bT)4KIz(wnnL?S zbXF8+-i_ZZ=YmfyM!c{4maR4avQQpBX6NwdWs|PfLHl8r&gm@uPXUzjWj*Te-o^*f z&z(ls4j#8H6BnlQH}3c$TjTRQxq3lYiQ~sE^Jgaf#%Y2-(~ldUbJO1eKB=3@`G1Y` zoT;Kkz0aob`s~jAb(v2D=+FOqUIE4z#9n|uPxsOGrefU*4k#d`VQw5Nh7-i|t(dyU zj+n~BCeoto&%fh}X^dNXtqG8rDUc*$O8*;gP1*H{<=Y7h(&YUpntjxF&OIFnn9x3G z%YJsK$mH4fS%H)e+M`bCtdkr&*BW?z;RkL8JgEIjH~a0^==R{to%;0y5--?JCO;Qs{Mep-EK7Z(pn~=Ud0l~QQ7>U`XnZ`{C(I(xkHm2A-X@&6gykRNvg|tc>|+0rm<;ne3X`pt z-xKABOJx?5GdXP~R1d}U4+XBu`PII9kC)iZ`|9+pl1P;>TGyXEe3D|(JKCjpODE3ip4Yua*cuF#cEGi9 z3RCs4#*4Kk%wGr~JOPxNtu)davnkILz9X^9ejKV^$`<$5%rj9P+FNbD7=uoN)1)7* z4JIl453CKNakmWkqbTbNB;9ar79%L$S_yhYLn6}bMu-Rg>}SXp zww#i(nlZUmt9Qe&Kdo#4a!&h>DNpB-8Eg6OW=AjQ^OQfQv=G{5B}=g*L)~{o{6}xH zZmoRW1;fNFgi-i3q)+caSTeoCK|U3%PEe$K{M(Ly*fevfH8bdYI}-Nov(u)+yhV){ zY`sfU@WWR}lt+WS6Ff6S_-4hEm$Thy{KmK{{T0irU|r6*6$;ePSP-wx+8$h=K<1uQ zSP3$ITGvhyMET>@ie*Fd3?g1fRxKAQl|CarjY0_+F6q|-?jV3%|2ML!8E>t(IZ%y_ zLpkhs$a~)14;XDm%og%tl=n~Z=OuBg9Of^>&jvW>r3rTf%73kVRSY%N&fl2)D1}$$ zOH=Wz$qU2A_i}GC@*Qw-3_Aq{t%>DzzPDAW#k`C;R(ip|K!@@<)L;U{NZK2Wpzx^_s zdYei|{X%D1(;LKs-T0^w@gHgB@_*`UO6WzsL%cK6;fjUkz{dN8ijv;j2)xy_@zCax zahTlXQg{Cf5p$GSeKz|cs-F3EA;kJuiU)NOCJNf^MvUkGz`??gQYw!N)F=Qd`t2-G z{v?b#{;+^dMOUQ8GO*mTjOqJRxL%Y?`+ZgC2k`ssX80vxD6KO5LiI{9Pj|(J+|=h= z0V6yKQfRk8N6QA9*ue}LV{7{Ng-%G9!I#9=0_7vdi473p8t zg?Sf)@$LW5Xkte{hq;~F=$U%Jp@}-^tX7$4MOiEBXZ$XoK2K^2RLE8q`}*cX+FQs;7gPs_ty*Y z&nll+jMU`+1DjX0-(39Lm757B53T}Ruf~688FE&|ptHTj2zuGD7xnJ{Aee!B3!Upq zx96souU3EXX2*P-`~<$~eH60)CG;GKz`HpGWZJC#eUBv-%pU^>ov44obAVfc=mi3} zcLQHhsR^KI&(neS#j~CiCI3Y6>nFWBSOZW`` z`{J5Ag-Go8&)sVMC}26|3m<3C9~U)NC+)%@`v|x2JNIVychdoLMfblf`~g`-M+}IZx8~d@1}+J z<(O}Hq?NpKqBx<5CFfMma)cLXn8rNGj7;mUjwlC0B7=1^ihs|;|Y&n!!ewS|HjTi$nA75 z%iGjNucu(lrWot4W(aVPBKZsIEI{Ve)PH2-lx`Yynq<8!|55rg4tD`0eZAXd`_1kxBN*Vg%>z7k06CG43<-Iy z4#~bbDh&o|edyU!_`f!9;4L8hfm;ZF0agzf;1kCP!p^t9Z~^3X*smXn;IfE6wbWsw z<1%4{r@J-37Hy}+cLpD|%ae5b(*u?-AThDs4Lt-K^|Sbz2;ZGAtqnQrW;GdK^)5k1 z5=P5z@{iZKbxzd9Hm@h4#!)E^CcS3SI-a`x&D^lMKO+nkTTL1!D5e^5lL$Q=p62nl z(W^=~vJY}8;)BDiFFOPBNkqCUd>N8wVjCY;UVadflczTu7uZS4NcB;C2|ICy5iH8u z%%WpgGXVVcOdYc%fIs6&oqiqc4zu4-+oa6lY0ea=IV8x|F6gL$?oP|;T%cmfx?8=AajfqSeJkObjZ`M#l`TDp z#1bPPD9)YqeD#O+x)7lm6E%Oi`J3DAIm<;7REz3&q^3>xeo)0g@f8DLw@K@Eo8^j{C+aUXqe6BPCw# z1dPIso*y`m_dggq{PJmI8o5Lzew#4gri(hzp872%f4~?~wDc*}xb@Q=5~h}|H2Q8E z`nSFGTig$(A}uk&)1p2^AAzVPc8-^{@pU^XGV6U9sq{}7GiSbuBAIkvtO5eiZ&b&R zlbSL!mdV(Opq)50nphT{`|O|;AD9Zi0uPKtpZKe$#jss#P@{QukL_5wOD+l&HWgL; z!=mgM05jRFTDUO3x9sjomw{@(T^A>-hzqv^Y#YkszwLe?XfvX&FOC^&$ubf*zaoVf zhq5Lgw{H%5dJ}fQ=0#Od`oTFDPKu z)Ay~{Dexhb&`E5<2Ls2mSSI_inqzf?+14olqT*b3syA$_o(3E!TTv+B@F1X$-g&0g zZjqU)ZUaNeWrQ>nc&-m;7CKo11c>YpNpS3(m2Y&8QpsL;I41`Y027V`3?gS|Q)RhE zRR%81Ki7b(`1at%=eXdQyV2#EPAxSL3o6l-nAgMoPa^w{005)`kvZm?Q0Yu%(r-lJ zRCQF)^7VNh@?w^F1oy1{_PRZ?az)tBj@u95pf@-@5PKm=>z>#d_!G;*IZ;a}$O5Ka zp4HOr;g3&5h`bfh=HQ6pW8tPH1+y|)rcO?N?PUYSG|2X1tVhM44C0o4PSqP>$x?hq zd4VcY{IM6_YY@5fy0va5(3msu=6t>0(&KumPE6@?yQX9(wIDH)y6tp!_#`CD{*mS-ksR$>IYN5WQO{{N`bx@K42%>v)*>9Z2PdI5k}4sk}tPJ!@@E$n2BrzfV31 zHhud9Y)KvJn~&m&e#V&Nu`FWUny1_+;kycF>Fwv~{|1$Rn?u}RSKAIsm&-tv-QE^_ zO%f^QYhmir3Owsl?dL=FfG80d`$Dme>#FIcyO+)I-J7E#rQqGx(=H?b3HJ`OXFGmH1eCx?f6vjED%9R+AbkAnU&i%Q7Am zB^{gnqj;+YYP{F^V=>Wr6d?~0g`H?j4Wk(%Giw4R46-jS5gK>aUXmEYRhGWJblW6g z)+88pBpmF8a7q0@wV-ysBecSti~T{KVWca=NcFrY#V`Dd-^x^J^Y0owI&t6Tse1Jq zwf8B^_=WV0@2loNW_#F%HyISCxL6xPk9-%y|a z*&rn`k(ZL&TGleiQ`{E0eW4BE+?*PFmBXo*m=M>@*x4ubnr7X}sEeHghY{S2S6rt1 zjphKn`OEoEUFCBMVy5kFF1vklWbbq@_l%6_^XL&rJ49AAVif;OL3dZ0NR>m9T!-aj zumdfe?+wjbQpC6>RdZEf=Fiw4sM!HjN&<3Xqg98La;6+T@y7+6HL z4&tQY`5Q@8J_SLjG;QIrXFVkJ>k$v)AIiJGDpcRuGB@?nFG#&d>!&b*Q zCHo83-1m~B1mY>oic1t5x=CONWS@JkB)AOMCl_dlIgLpf9mGb?;rh9iTVZO&A{F8T7fUc_fT?v;p8}9ROGHmag!v> z2;QJFQO=?|F$Utj*XA5>q24@acM}yhZ)0PDJrO%SEP=nY1@>zdCO$)&AIBhmyL^!+ z?1ys~W~eWNdRPhtj0l1s_#T+xj3OxSMK>$F9gV-le}#XSoIv}wFD*GywC3EAsyorI zKjQc3@c6@oP2tZGh?n1pr#Vf}qoXlcernw{Sv|j*zYN(qeFIaSTW1{+_sWY(9||My zvS@wU(AlZ~j}`zlz&*IJA!%V_*EnPxxhMIwXSiYPqxeoAzJmRFoK})*{m=f=g$)Ng zITHtc+<#_EaWG3ze$qJg1tdd8)VbuwVEBvai(^+utU z$uY{(VTCUB=$$|0vkH_cjW5ItNRp_Xhn_o2z^N$@(^69#k;L7kn-Wp3E|0!=FyGN; zhHNyIvrfoZrPlZwiTezv5AhTjxv(O$M9S7+Fo>I$b{Sgz-M8u|haW`QWfD!KPWlvF zwVjh#ruleMV}+upT+-U+%@SLkh0;xW z?s`g?jxNm!f3Xk>PNc^%W&K&_CJU!VXb3+x|J?;Cdbw7Hq^Nh3OZ^p7XI495aFGG@ zVdKJNv%$^N!e0iVr5DP{-zrKLRR~8D@MZa^YooM;n#GS~Z2BrRXEyIQEPNTEgNIE! zIE-N`EtuHmDkn4ks|Z%gu6(x05Zc$~vouK+bdDY9XgEUcY)d)?#BN&BIMXbXPTjO@ zFHf>)4k6B^6wlNDMFMP=6n&rr<6e0HzWyj+RJn*xt>p(Fije4xzWpHx3I!1mPIGe; zfxDe2D}3(kax(A9x1>L(*CHh8LxpbWF^w|g*0%~P zG(hTzDPS27y#}g@6F6mh2VDD~7IK1S5+slr0R5&t_G88t6X*GLDOa=v5{aO3+n zgAG5CK=Z-^OYEp zs1BIpb!sl8)#3k~>%6vLAP}F=#inS+Ld5G#UDu0w=fhtxRMtt&XLOr7P?i?R7M^#d zrvCF!1tCnhAbYV$-?ji5FN7XuFs)#EJGysfM|3m0uE<+*9_?}c`Ax{dX#M*cCI29^ z@1dUSlv%VneBQ}yQJja!=>ZMADDeRDf8cgbf5-bA4N{O+@Ux}({6`8EPRnudn=?YySGYd5IdbmxFRhmbhT(Fl zhDoaoo~X}$wr?9v{k+#8obIbK4jYx*w8oKR_V69D7adLE8FwAhEsfr%MdY~ZXWFR$ z#)&zA6e>I>A<#n)efX3eh;>p`G-pX44Zq}t`1*U3fk>kU>c-rs$2OtHkc?L^#GfsI zyb#4hI=1`;pVEx!#l7rcyhQ@^y$b@*A|EiN`M=6X0*F53>wutbETL)nN``6hqb>k6 zXId*rSC(qg>YbTDQUEe@n}A914wFuFKU@!5J=1^;kLUse+{!nbrr=9v_~zxC)aiHp z77A_dz_o?Y)_eP5LK&?T1!{~d{_ek-o;tq$W~-gxto>T*R{f1bb_n!M=*9F1zLZPN z^c8F9*(x+L^f>S~VL<8V2#mY{=Bg2lQoE2EurJUAH5ViqjGtU+;MlB}+G}p3;*Err8)QZjn$(V5JSayT z^?riT(+61T20>K`D`p-~urwhPkWe9oI}Itd1G}KrjeVRST?FNDR73CEe2~49tD4*! zRyVY^(%K|<$JUxR?Cq0=9Vw1KWtNd* zob%G=cP7_on{*$R@8UYn&E}=7%*;s!?`_P4o15YDbny%VG?d)n8k-lh%t;7G1sV4O6amuve+a0d?i6 z1y1~sd_~#}S_@u|!PNu?ozb5wr!2C80MwRn58Dh+Y1}35o~iTe_kV%9;Co;4x#6Q+ zpV;QcCL}q5WS%d(xRWKldd+6sfo8NTzi7eN)Q3C?eUu( zOf)9T;;NhMwX5{0AL-a6^~Ydu-5A&|e}4|oI23V>3aXXGeKE=nZoQeRN);l10QW$< znmjF$S&t;&UCoG!L6n=zu;msy8OS1Qg})G@-R^jLAB3f*QiC5Q3uRPTDrAwa*G4>! zQ75fNdh5d0w8`?zCB&Q}!ghY)rdebCE6vjGNxAOvmd|;1VNS6ohJr3-e!#GqXJV%X z*>+F$_Wc1N)K16`#$OmmY@BU8)X}D)=N|6%2Z6vGKXo2MUSOJXUNDIqh9_R?Ci_S( z?Cpuduz?LPNg$3uT(J=Rc6ua)B`q5Ue5Vh;Uf1Bu+e_ATcXNYTfBt6wxb$&j^VkLZ z(b};U{)GXkUiW|ei^a;m{Z&J5TDtBY!pB{nUiPVM(q-ZT8(431vA6hG!mXO(d2&9q zxx7YpB_rPJbP*TkYw^WER#w*kbR_iV@VDd&c2L-~ezbQ7Lx$o#M9r|*V{V=KOJs<6pe4~UeASEU@%dM4W$}@t+ah8q5XWP z-Jo`~=2(P8DywBtSCYoBIduIa!dyW%wzgT{%AgGoV^ZLrM`KZMt+mi5cCf7Um@SEmAaNw zE!0giz5E50_PC+D(Py4)pS0qu{UmXz_eft?_B4wL|^_Ns5A`Kd@q*^xB7fd&XL` zvVV`X6(7bu37s~lOtqWaAjux8xI!9LU+SjK7)7m2l)bRJy_mEDE?s&zeY~mX0QmHF z$?7UPF)>l;vPLNo7$HFrLmRdSAk{4VIA8UV@-=s2<+pRBO?|VwWh(YC1dVZ42ednLx4~P4KIm6`= ztHk1?yhwds3guK9eJ9^mY`f0A>iA*KFAWmWeE z;4J4;KTr{4;}B2%%ydQi7sUG%#y*jv$~FkG4-wjuf2kgFqg;P6v>M~wd!au(-N7H# zbqS^25hF6v&O#4kGzU7-vsMQz)!EG;^QsGk#sq0bz3bHeWsT5Ho-j^@Rrd3i}vsir(+ITB!8)yl3YzO)Nbh)2Q6jq21@kNozs_z{Rtyw zzp{#$5=aU+iTvOqjj@h`9f@P11mE4rLBxgeCh$*kjdBrBekK1F*Br-uNY&&3c4RPW zc3ZBqu@i+f?g(uLp0cYHa4p#z)2wU{;1Yw2us5n?$BC~`z*r!b!V8j8J~a>Y&aN?K zfV?N5Kc+JwV*1RZ#JpZUK?qOJ5qoG4%bucQ%PXN%lNPq)PO-+eRT_HLk(sY>SuSEax0a_IeTX^yXqwOjO;Cbet{?hm3DLaM?e`W+h+}GAPS1xWVPhqXeT>YsF4lpdG*RX|8$+vg4-VB=N z9}K<}>%U1Oc<||K2yGMV@izvPQc7pbU*3%}(G8Z3;4l>_g1iDvRoAF|KH6SkeJZ4e zNxbpr=Z)(Y!&HmzutRG~FFku&?%@`Mv-x8V&qF~k4*A(3j8e^R4LwiJhp4DDk#87XE6`Zef%RNm2ZPZh@%wastdlBXYD8LLAU<1`~4 z2a$^pjnT;HEHxV=TIR$(OOV3AyXyc@HsOPu0)`+_y&;+&x zX#!&(4o_4gDk?W5O8ti~7ozzcA8WXOUN1de#9EiS-E^QgnPkGW#$Q7Lw=Ylmu@B!LAw(OU^C&%MH!WWzLW(E6%a$N};_0ZF zVZ1m=_+#nyoY&kdV-L6~q;}?B^9jmu^FwD_>1C>@`4YQQHEWv$id+k=Bjdh3qImb( zQM-TKf={JT)P5M1Pr$}|BHc6zg+M6Gm3beLN~rL2J`{)LTs zm2A|MawVMtaT1ztwnSFGyc`@<@TZ3SnP(lt(wUoIFP`}Ci{5^$sf?^<*n^a>7Ff*u z@4suN84;4f)dK*TH6lOi>-Ht#i!+D8mYT{&u@yb>%`4F$r`v{D;zA5FJ-}+OB};Air8DZt*xM&b&+3 zOo*E%5f1vQue~Jt-IhR$1^1c1Qf1)rA;VHTZ`^R(KHWT!52NU}+!yOIS0_Y+bA`4wED~yCZsXcGwA+uTZzx7n zti|5PHQWcxIHwQ01e0qtESZ?L-+XgRgofomkWO=kq#USO#Wtfs@?a3yW(7|KhRGTp+8ThxpDJ?;&^V`w#@=$?^8zg`I zMup61e5cwalu=IeMZzz6S-{}u$Ehun5LIhT4j6Q z6`Z*aS+59`(`kY3H`Jc~JyfgA&(B+yBz!Ia3dw5nicR&HZ&M)Y1_B$^U^l-vF&PIy z^HPWm1wN7AYX2R;WY+%$d&{ijUiq3s4ZcJfqU%sWL7oe!5yjxV-OKE&72aTmfz)@G zZM+GSx0jQbZSq0B(9)ZgeknsOVRGlP(&IG5#Fhjwt}I~Dx!A1X!104(1+bAZzPe6| zJv+M4Y0Z16Gm`N>NjNjEHQ}F3g)3pLn^7vS6wEj6hl=d7 zcpSxK*uHJIblPb5WJ=(VE3Aks-ieBpHU|JT?41}p6SYk)#v{y1P)zal>Qs8i9?oSl zgPomONI+fOKGBv$m3v!jVv1jBF1#EvxwUh1Lz!{W20t}0k_JLf6bjZlyQ$&TNMQpP zuTw1(m)4LS0#)go|HO#~N*PuRujh7mB|MIpIcb{Ywin z*xQy0sI#n$vtMDUIbQqviXR;VHuzeTe7v*8#hh0GGelo!DxsM!6_JD2N$0)mR(R-Y zbcN)%;SnS$Ys>9o?$*iasTvou5|~74jHD^m?h4`HntqJ)89v>eoj*2e_Pm~L9uolj zaHzL=2g&DU16c%z=1wMtS~`i|!|^4QzGqK(dAYtm$CbLuT;AKeOWoCGCTP?z>jwC& zIR4Z=1;KY`0kd627X$rlF!G$eg|p#1`sq4js*I-SU6f$8@K`CLXR9jN5uFVD#~m`r z>p-w9X1BH;f~mm5OI|*vC8A;Go;~y5x=x{H0J5OvwCU7~p^L@`ho01A15PWC_UpRl zNQV?U`om2GyY@FrjKx!N-Ps4Yx<+|&(?-1yH`N7CWx-1Fb5f&`CATgMEvwz9~ z2gfgm^w{RU;(7Fo{G=r;a0t;Kht)zU^+k#c?Unsb0gV_*L{e%ab$lD^QnGQGksXI_ zS4O(p6No;yJ1S5a1t~{D`x}9Korj5wO`ErG4{B@tVe(_ttO%$a_Oa^qlX=q`4ecC0 z5VbqIw6W^FM2IsgI9Ms=`8+vDP{bd2yQP94iVJ1w_`dk_Q~$Q#SUKw z0;+SauKQUDz81K3zLg?|JJHQvJWeyK-hZ)Ao?OCCu+6)78oXDD08^oZEg>&ZU`Rh4UgQt*anZ#jE9&-e+tW>=XzXwIR5i)cECP& zw$`cs&(a}ap`i3PQp_k}>}n&ERqntlhzYs6(TTs>SP^{G)N8BK#ak5j{S0`z<%`huN?f^?{m$C3+8r@#=>4Bv zl$&eX=7J9`ECk29BJq|bXNg0!y8a69uKaMjQDRa&pP~ke@qcQ$QdDo&Ixv8}qqCfH zugB^pIX)So?r{J7{i#~X)8_Blq0tx2AXpO)_#MT7k~6<1A!`_(Cx4oc;fJ-1p{Z{? zPN-7nD7UvtZ7;s|Ph1>9y8gO{;jtEEol!qe-mlV7Z2@l3|9BHc*&!E$RyPO7E|cVB zWI&W)E%1H-RXZ4{y0`mrp-fhBs#v|OY+mD|u#uU%;Nx-A&_k)v!@-K%K3>KBdz2-$ zZNR$C*}DwBpF6NNw@?c!FJDf01sJF1lrfBg<58 zV@w#iBB*lONtPXsfa#oXav{Xn)qC8b(M>9g@e21icsYo>ux z*gv9**S6W{JHs^~NlQjZtsYppvqg<3Ux(kbNsW>dQ2Ua!{iW6wHs74JH?Z~XhelcH z`c@}glWtfs3JQ8#wUh0niIU$~z+L*)gNRL+4=)fx=k>(M2O z7EYX1&-W+zoO#&W}2_TD5VE+sS4~quM=X#ySR$7Zqjao< zQit7ix-3q#-H)SJ8GU9v4!8V>#jBU$$A()SHBV=L=%)NXrrtZAtv~$x4^>pvu2rkC zTRXNECHCH>wJWyTdwwWURIL(%+N)M=vG)i{ZDOw)QPm2y?$i7GyYJsE=RbKE=bXIX z=en-f>-lmGB+!!py4XFB!p&9P^$4T=xpSZ9_^fT#(P6aw=AR$EE{Q6c;+?-oyI5)^ zKJ!=Per{}R8Q1svhdS1w2eo86o7b~1@1P}QW}o2hgAK0&ua<)s&wnoa#N?G^$0sx& zoF9LfxEhjQGIjap_Ow5aQpVdso%g-C8rX&oy8TGP1VTim7o})eXmsq%tp3Bm$)<|z zowVl7GqXsQjq%sDk%rD!8^LuAv*jZkX!cP@noE5jld>02V_&t<3!XeRRVJJ`OtJ~i zl1jQPRBpe720sZCsn+yO_tMdpKYeBJCIiMCO;34ZyyHS_-|tlyW9uP735#gWM5MHu zR+41|Si@9&VhW$(@|(-y=(Hp8d}F7(Z$c~?RLhJiH62ScGtg^&L0WYSU%=AuKKr!x z6!g$H+Ub^f2sMY78QKpkmJxnRy^n0J#HMDFDyQDf9N~R<>XZh5@@%)zOsRCXJFicR zUwz9>3GQS6Tn)-3rn?3$-RDhk?lgN}LBb@>cSZ-TH>cwQ3lb3XQf?D)Pf0uYaM`Kg zo4n*>f!PRy{`@>6XclD%6qdNl%WrrIjRFz+NZ}fG`YfK*s!~;t#B#pyaS(Z6W_>4$ z5CbPpLK&4RN7R4Q=2wMoP_H`p4NV~H%$ph^gsc_y-j3aWhI&VWxR( z(;n_VV02O+e-#6j5j>g_INI%2XdJ=JDCEZiNv#(b7lZD3IW>kApYn0X&x5@^&B`-3 zdzbvshXC(7Jlz`JuKvAHps8wy~rx| z=*fQtIMXzMUa5W9^<3DshUMR>KL--{p8zn?EWjMf4F%+zmp^;S??f)O!}1vdJ17Wa zH;LaCA-!ZJB|$Z2l{Ry9LQf&~HiQXaNe@DG0(EG?>w84l_=>p?n{iMYJ_=C7Y}+Bo z-A$u1D+K~3***w;rT!J*I)OvKi(;Nu2di@o8q3|IPJ~ajqi`=eo;j#xq)eNwhTU>E z%mSe|Sqp$+!x5BwGaY795d!S!FlDQqElp*hUG?!Bx)uJ2o3p~$hk|mLLd|uFfR@6a z0#wry6y+|S^#HQVN7+E~*%aDZWxLZj25V$chZaf#XNh2Rreg7QC*c2R0Zb(9h_93? zYoyAhb;UUwXUi9z?aqMdCWlsX)pQOI>Q&)0@(^p5IEi?*y>V>l^^Zr;`liKJBJDhK zmTWWmQZk>eJY{I;?j*ZVLkpgP5O>BHrhga-Pf0@X*0B+o4m|E^pp{3)4|66x9GdQB zA4Kfm{n>OsCV`n;=tF)t1#6KlD4f(CpSt_bj&FEds!egDF}R^HDd(6zCt%0%BS8Pa zmz0!b1An)UrhG?lYyOg(ib9>Y=K_d(5~7t4{JCaP`3JbVQ19avAkqcf_|khx9}ihQ zj*#-YVpNILn4_nxhR|nX*VH86~;v29MCDz2c+d@nmRfYcovo7ctVs?3SnBy zV}C0%$w%!5(OORMZw>q}4J$azwm4eMMYTrS=J=Qw#S>6|V^nJekrgoCqmcx&gPWTU zl!Rul(6d1QTzDo|QEHW(kC738;pj(`vn)F=ojnS>*+l~b^07{%ncF{M7poxn8^@*F zOSx+m%8QhaC>*sN3R3ymfpYyX*p(fj}q6(_I7rOmY_=(;q-iP0Dbu* z?eTHEDko6+i#TaXHYGw~JE5ZK3?oRFM~5`g7r1cUc}5}YXAa+weGC3u59XPoned$7 z$*gHYM*AGSTgk!)93S_2DU(HLe$)5-9TH9DbcYfCq<#@hZgii*Qln_RJaC z1lM4AzaeEFTjz7-io$hY7mV}2F0@_-2D!nsQesP8U(rUfGe*0P2LNf2&}sEPzw}Y; z{(2I^PJv*`fg~)tQ@xB#v50=CoZ#??l|paw1vENO=oC;j=D)O1xx=Myr8;CC&>;Wb zk){M+kKgV9$hX2G>hd% zaeiKI`dmh46Pejg>i`|f`+#rN(kaw`;rpvrh6ka0<-sW^!h=AHWr#ge8``N?y^RY~`Uz0qH@I2F{YAEkc zvD?({wK52~E>$^ZS|uQJvithpz((7}i>CP>QJM;*%gl$=m<7DgnkxL3rmwD*4hKN0 zrN@`RqiP0262S9nY8pfXU6rYXl4{85w>h%|CnHR@6h1wj2F+afO~`n}lDW!Q1s}^O zol@O-{sG6uOvNDOBL!==zF+4rpcVqEZF^3-IX1zW@pwNxFkxEBcjTa<5PBh z0UJZRrUXz0#MM+F7!4~kEk-l#!M?%Xx z7F>!M6&8!17rrSfe-c%(@7_VEN%!vV#7Z+>b?c8x<5D$sUi+=!=1zOqXjNrHvU4Bc zeI$#7b8u;wJ$#&hy^Y`X^~kBx+$r8*>n{}DEI31Y@Mn=J?mDu{S?>CWPDyJ1%8Fm- zr8#Kz@<1XDAo|3xB`?#|A@B0V*=2dHh(1Vu*6c^#vwBnUmP(JesPt^tp6pe9Hp1}Z z6An>4^9<99NG5kqNhb#mY`h5m5cdo1@V?!6JVwP;8phS;b6SCaTk!uJxw*hd&TlmY z*H0%4c!q@xyX3z}@BG%hD2`CJ7zqGst?RZ|RyiN^5}i<((4XBzfEND>LecXbUG!~V zk1nS3F2|S!v=YP@mHdo?^&iyAfz9lM5?jGy0g#oV;^l!&4Fm=T6 zGeRyKEUy7!y$SB@O!gEp@$ApE!N1_9P9UV}YHamt41j3>_6dJq-(rJxoaFAx$_nsi zK;>-_da*rcahdkd8UYl208O;k{9oG>sL`sg{t-DYk2~dJbo`U?vvh1^7}-$ij5R&M z-6wxAv>5?}q6%wtQiV_?y$GS>K-;)J2_$o8#8ysX&{N2Gy31w(k6nn_VeaI_2tw_) z{6tTf+4{47st7v%{J-ZEQsbhNcR^cB4WFfceod9mecw9~;!MW;oc1qCxRF??hO*4U zHfq9;Or@W7QnK7^Bm*W18!h*dA}?1fZQu~H8~i-Jj0-8Oo_d{@C#^?ppt0s0p{&%~ zmlhL0gEWw{=_c7pvMwN0wW;&zR0H=bu$PuKNM|7B@nZ-tqZZbe%wWT#=6g;zoec2e zVArgz!(w~JW`p0eOxSaInPvwjy~?F!Zp%h@jHDh^E+3Bag$Z zT*R@=>b=m?ykc`S3qumMFmdg<(4C~-cU_N1Q}k_1Z7DtTk)D+gUoSP{X||Je;Zy1h z9uBw7lDswZ8#4t7D6;;E!4pc98V8=N8-5I^^G7heBS!n85a*$Pg|2nU0G9z}H-on}2QlHJ!-kI6+(~&Q8Vr=aDS%H!TRzeLf zH9&99yX#9&Xk@>GT=J(#IV9Jc76s$QXotFY4&KuAiT@JDCDZ%Rj7Z7EA^ved{RoARa(;w%pRW&TxWzJtwVYDxAe<2#A zYCRxC+Tse$FLOmP!*=SlRh>v+|ojmZpRH9N%XN-vaBwiIGr`1Q= z7GCn&0oYTcrlkgWGy5(RQ$x6}Q9{W&Z9*g>T0t~AlqckOi_Z81e98D!zQ3JtA?hwL z)B3$6roZNScw#yQbmMsHD^ zBz?20b_wU@=wT7Yy@T(3h+&mkLPyuu25a|2r{gSHM>DkIOV1Z5&NJnhQba6kEPiP| zn=1VP`F;bt`0O}@#?Z?^k(k@}syvVHIUpM)7pc)UReCy!3_JZ}2F3b$jBQo>SS0-M zsTAw0W9t~Zhv~t>^TU4YHez0W!;Ti@R&S80?#I}qldvQU3W(iz(e-7|@sQQQtD?oH z-x}_NjED^4e}9RUc{EDpM>kAo(y@u!c^6Us*HCYO*MsFzux}v<9QPlWwtFW0#iR~t zA1RnaUE6kTp{+H57Q}d3fD)+_)nXWF(`QasE)v(Geltt;+9X_RZn3web*7xx4k83t zy^2WDEoQ7u4E9fpg-T1m|=>**=#?UBIJ|5>qtbvw7Zwrd)50O%Bme%4I{|0Cwk zsAafvJ=d9;rgxK1RY5(oU=d7|t5)$csxY6!Yc#8L!~9*_86yu>R>RQH@6}Dn-e((e zmV5SmLaW4T14&J7x&b{*BH#r*E{~M!usST%<CSRK!BVWE=?~S?kt&uin=gz-& zdjZYU?eMq*I4t_yjI+3W05Ox#64CS!x>pwy(qJZBw9l2A9;Cs^WYInV_LFTpz?PKQ z!x~>RZB<3S@p~&tBu`8E&i^x3FLCaj!ly#QJ6va{Xe1R%#mYAM*qt-q><$gtZ}D$P zkJk3^3*|M*a{^l)yTM`w8sa@(lA-S(f^~^eK}(HfV{zkynVi^9eo1by55WQI3mCd1 zg&Cl`>C}K*MQv-ZUan+ebL62PIIy9|H;UZNLJ&YH*Ln%eFchdyUD_>B1!M#0rPDMj zlIr(TC?nk1{xjXhK1XBk$P^)&g6Vu%FU%d65U&sgPW94OP)kNR@tajh@Gam_N^V>3 zEo{C|olgU6nsg3b0PtUtm|zd34;%f1Yw@Z1EG}~yKvi|*>q;CN>0_=D_2+Nf?4oLU z`CM+R6RhYr!0o^u-v~f%*I94!5Z8d!tpjyC$$CBMi@H9L=%`~JdG)sa;jl-c$sP zCB2 zAGXB3O(8=g&OuUd>>Zss`7~FU#(@ER!nukPs;_gH5nQ#H41*_+j~4 z+auerQ>pH$^L7t3`yOTH`Ce-H9RZ#t)EsEf*5#AUZUwbb`+y16>C96T&@*W%$~*vU z12g8&dSKbM1d5B7r}tY6eZqUsKm@H&|1PEt|w`%Vq`j^k3?*w#r@w5Y>((KsbP_ZrsR z+?0nl_D0TkdGPCn50sb|F$Pm+S`F4aMC91msE--h*|~+O<8$^}-|16h@6kP$&i!uz zoUAXSGP3yxF2R@XI{stt98^>6NGkO7qzP`w&#ROim0PWYxcJ*pp`}fUTsmt<-Qn1T zMl@=`xDX(x5F0ZID(-PQdgszOkdf+3U{1$ zF%m0cdX7XmSA(KVD_EJB5DP+vAXl357b10OmFO?618WH^{b)xQVHiwPA;34Uas&}j zTI_xhGe}!{d3lNc(vZP$PbXwR4Luz6VC7J79{*SlD4LC4BOV( zN$^F>cDv2-T`48)L&_Zv3O~7}FYr=xKF;m-UPaK-m=(u?>#?|cG7z_w@V|pFuztrF zNZlnl7N4icgnx&njFe-U%U3%l=)OHRj{Z9Uyul+?tn}H-Y@E$ga~S}uNo|>QlSrnQ zt7CP+nR76dZ>@g;Tc3*LH;`t2lzf!+PW9ne{m{mc|9{X_bWP2el;u3$LK=8^Muh#_ zb6-%H(@=<;3dY#bMiJBr3iz)pFrW?CbHfA@lU6K8idkoy-mc#KJR8e%X|1XqEZUPB z7$8B`{zy=jmxsZ;EL`_wQSYw$b+`*A9>p_I0^VlZ+soRZWY8g+FFYLJ!v-I`T9uXw z@doUT|59v$9n^EqN9%jr&z}2$0dX0~TM^98&Q@|c)eiwFJ8&ZaqXX43 zz{2~F*e`dpDR(p5)ZqjC6jNBAZ=sxv3e{F^(1{xTR_&LGL3yUE!ge_9@o{aky2Dzt z&?HCik(fOKMERj^Ky62b`u2f8*bduN1lW9ijhs2eg1(C$j0xFn6W}Y9E3K2*@2Cyg zp{o-KEYzs%LJ@QdYTXNZVpe!S}&3GH*fA>ZEz^9_D4bxA6Qgj zLm>^m69m(ZQF@QK*F@buXqZM#>Az9%*)bEejCjWYG3v*7xs7S+UwwLfPpO?CY46N~ zQpP}=<|pRn^yMvgzSQt!0f)M`5ZH$! z{#aH@6Fz7`AmZm^Sm&|C5`fDym%SxPWMacpSYtsmekl%*F7Va%PAq1)r?xSRQ%q5T zQAsVPHZ=%M3T>OJMDYsgmOp1|Ft&j_D*CiGfbB{xc`Ni3tiWXSfJZ(?RhvBG*U3l# z!ME7#b#1F?R75IIB zE^YWl0KT(BM#&i6odo9`H*SD$onwk08u3S}^#nh`eM<~nma_bvdr~4dCAE95@{&Wv zwwzSh{`s-=>EK`Sqr%m3?KNB19KW7+dQp%;__7MNKRBCJiu+~dfW4cN*eyEqBZD0Rah6Ar-*4^e}|AgoOz|jrO6pQoE&ZD zsa=#RzPpb`*Hb66?$U_S5-L_C3)53Y`UcO&#}2$n*-yPty?9S%dQkKF5SXhel_P+d zKY0 zOwjk-yA4LJ$ezyjGQDqI5J44z85$m=Fnj`1qY~O@+E<$4Oo58W3u4C$C)GU`Y}H?o z{5AiLjC*MwCBTvuUwoSQRXal!a?0-2X~nM8lxweOlRH7Hhj!??*x0p1RD1blpqJCn zWpPARC@A-^S zOcEetIfr#emW{9UxYXNNzM7!0qkb+o*tRc=*Vtf{#OrlxJb?^(plM^)bWq^Cl!C~{ zgp`9suivAv-;}x2=~>z0+0hA{6U?%lT+REop6(XTTBe3+Lk3RhFQkfAYhF6kls*WV z43tt#3!8lnXh&OW_p_1t7tv)%Eo!j*IS&_fYaB{3Uheb|nQUyy_bPmi)E*~&YvX^| z;%ZcxK@!`VuAiBz!K;=`oj%p1Ro+0AuzlUtk^ahWA|YcYwIx-7W8bHAL=%ig7HpS% zGpF)z9$Mz`Wv86!L6@h&d2ELHU}^A-ynSUtt0da@de(x$)BX$MPXv^sxL98J(A=Td zwDH!{6a265G=;>3VYE&ORxC<-$CU323CPG#Y_26}VA>eXha{f|n4cBn%B=T7<3mzJ zzru5vk_jF;8a3Ag!~6d^Zhg^c&rfDC%c4%(*SIiGPtUJSO@7-W4OIuUqua!vKA1wE z)z%*U@&^{*n`IUqZSC2tIzYtzC(s9I9+`a_?bmI*g20c9&F-%$E__;?I}J~MP_vs8 z1Y4m`c?dc%@*v-iOk7Wz|L}RF>c~ypw^QtGp4?ewz$i27%lGf!0preX;O$NCKcdIK z5~rfNaUQ_*F`5O+InM%|4)8JuK>p;8iO0;OKGQ|L{H`W5dFkV&L-LF#%iF5k+_%Uqo*F)Jc9 z+(dvuf^xWJJGyDfRsF|1I@vroz};drb71lTZ@Dd?Znkyt3!Y0;JMr755cT09BQ5CP zF{eqGpGp>U!vQVm3u(i2Z?B?}op!^#z3eH2#NYmjkPilz0QK?yTIb(!KV2Xoch5US zd!L}y@6s2vhvp;oBfdjM=?R0_z(j0zoXQz}EHTz>o-cVov2co1`nAtR%|RcK-dy1K z^*5C4;g67?X-RM{q}(?6ysvFy+^|?A{^Te&CnK0j(7^J}t4rnkdn*ugd=r4df;5n|>1p7hQ)mEGe7+@5V0cZIn z8bAybQBcJD-%0vikx(J$|1*5(c)3Lp`fprgjbd=($A9WZNu{B-?EFb{d|I@@DJyb4 zN_XF?7z$OxFVOvnaz4UT9!Tdud6DpQgvBlOghM^*w;!R>kOO*MU1Q7XXwS2PL(c*TSFT$hTg z66mMWJFo2gEGv!8oacN-w>|3?wvd3H#St6`m^PVOF*)}Oj)P#KfNg3>EQ8>9wbjTO z<3cnuHhJRV;iXpIiViAY0rF6rmYjAP$jUR91WND0=%e`|GSelF_ z5OaT=?mASa5tnJ8ZNL0yddHXGa|6{R(E2^b0;dN=e+KP)M4<;BM-!@-IdTY>^cFk? z68oexf-wzObQcuF_?!{`?iA)^F);%SDO~^0J#KM$;P=$R00OO!6 ztyR(S)Qk(D)`K-#R2wzSQf3f-)YphAyR!q|$Y4OSugFnnK!-3(;)n&nJ3FS=MSvZQ zu@wjfF8lKJ&KNGndfuf;{L|fhyCylJd;i_ifUrPUNCg_g>P-Orm%iP;sE2AN>IBA{ zCV+!+Ks)shXcK<1>8NH;H9u(vro*xtS(m5K@LCd4!RGMgn_Sq?c+w!IA8 zmm2v1-@|Xot%;+w5#gOh&+Y-)Y0_cNK2zKj2Z_vUkJp7?l{<_V1f}L!aXA z)>%H69Vm4jx*RPsZE~P(n0++oR_a8Y>o#}rj5h$EqW8yeZ}-*6iM5gCgG{Qo>aJ1K z8FzUF;a6+!a_14?WnzgI%+eA&e%2u0@PAM@*maId{^7{ z+Lzwu->F_rMlc`L`FQ7B(?gRHm5q<6EUKG$i(JdVQ$EZ!{7a19PmNq#+szK{6A`&A zHx_=_wh>ei#O8SY4j$>*62Jd{v;b0mKvX1f^+0wUp%4G4yO6!A*2aX~t_k_)?u(b) zWSG9Y!(+#m&sV8R{Pi)%&x|~Gc6Q?aeKcOFrD0?y`$!krgS+y5O|sH=vF{W6$@VsJ zTK`uVl74pjrq)hgKA5k~Ecc7Cy&rZt;LyvUs9vM|2O#a6XkiF<#JFj$^)>vK(;&Dj zPxaV{y}``ctq?GJD;laXZ?e)5+<%Yx{NUQQ#Gi`qQYB%}W8!Pdv-1=CN73v5%vgZi zJM?EHYa2jsw>r2`8Ow%~IXV_evY0 zU!E}uXCkO0vM2naD9fS{@7*~xiaFRccp0Xd!6Gk>3~9^C`M#9z3?S8ixIT#$rYnUT zVq5Uvmlvax?*EWU#-4s(7$Z!WH5L?Adw;A@nka_3!d_fZ*k?R6gR8DnE$tNlxLi?< z3cysOs^VIUMHgu16xDmsMYNQjb6vbFOGyJ-N&Q?c#P)uII|>1H%*(@=zIB5EkA@AF z+pISX#h34+j3EWgQy`FhMAt7lLK)g-?VkO_Kc6r|8}mo(#}6OM?r$CD{M@i*y>1oC zs>qA>pWGtmDI)kv-1Ll)riWLR8#|87piUxH94kveZXi$N-SObBUAWPzt!a$RR`>(Awi=m~-S2j{B6d0jbRZiymq&yGx|9CnM`nA<}q z!?YAh(cSUl{R6x)0z$@J9ItgS*oV{0P8+88C5v9+UR~d(a>6gvAz!C+*w7A(|G>6p zslseM^LzwgdbGK-Q8+UpJ+U5F931L^TS*Wwq-H&U=`6arZx8E?hsn{(1Wb`1<~aGe zj{&Z34b45OV_h3K98FPEhM*KyR28C|A%4!Y1Is^dmJ#MAyl?-pw5MH!6P)Z$`+a%(aTidvbZ2sY7Le4&e4#E!@aRc1?&x zhlE>cAwqHZjATI(h?FWL{TUw&2=ojPW)4~ zz>Y`vY^lX|qUq-roh$O)W;Fag|W{024fjK>NJCnz9`At ztID6GkU`F8y}^qYl4+9oT#r%yC;0|yXlb2f^ng$y9Ikub)xw;?6CEwn9Se`$)N4i6 z#i1Znl=me1Hd7ftp4!^3qTvLxC}QiXBta?x%5!)H!;FETr$Ig}_WMik1l557@>_=c zb@arON1LxsMuHV`_rLV*8mK4jTV}Y$3)|W|tc=eLV)E z-N_gtg&E^Ps=F&d9~ai$&I;wWkY*qI6O}&d7gSFBPbo3IxH7YSADiK(7&n< z=G%W{8a(I3kI>+i^7+7sSBGRAR1JOaM*UjaZ%$txX^JM%5HY|LM!!Z_Rh8bAqI60% z^N{d3CHRA1W5D?lK_w-XHK(axp*6~K&hekg!M8#*8EYhQrk9N`Ew@7cWXW?`gD9kh zqyd=Kge#w{Yv{G@vXHQy4vSV5b)@q?`{tspuiYu5h0&PkCzLFI`_#*XL-2N-1C3U; zT(7FO9n#}JUK!qMs<11X3(A7g&qM!L>KtZEYvsIVMELXf$nQ^(p@GeF?D*64!O_j_ zliaiOLi4tcpsQP};ra{{oZkB-^8%{z#kU%kp)1b`U$w z0VkwT?dfB}w-a*%B8CYN4yW>mw$gHP5A$Xk{R9D;Q%OZy)5_n=Ki_120|Icb+{@Rz z+p`Pim@k02<-uoCHy98_#01*7Km3))R|`7h{KpA4F2E-+4(PddoIi|1ruAOJ+`IsejCG}D7f|V|aIi1V3*g4$U-UN0NlTAN#qhma3%PEjm|WQR zrjG_1lhZ9i8IY@?m#!mYtDS$0*|oll9$M+D zBN(ZYxcs1^qz$uA+$iO&^gV9E7J`~AG`?&2NL~z@$D_YB2TVIXIC?Qf+!V|hx zKW-7Vn+8Ix?hg?E?EI$lvn?<*-1Vic;+>PpZ}E9V=A@*q2F0Ra&J(cs!q|Qlwl(`; zMFukWo>&eF%Q2tfC*yntN%p{hmtcPRFKn{b#!k5p7oyDKtsk!&DM3h?#KCnpf(a^n zd6i**r%M~e%diHONM%pCM&Uk z^=`9hsY3u*sf#q3s-{;ERBJ1K&C3St&DF+hBgMK_HUVfNqc4m-Q- zyuC#K0K9cg%h#7-f9`+-FTdyN&#d0|%9S?!lYn~p-G!W9CdyguSl<4z^urO6AEz&N zp8Qob;%rKtD!{FdO5KTVkqSQg_C?nJ=jVgQxw$#O&v*4BcN<9;*RLr+^G5a{@{Yvc zcb4beF#oG{NcioQV8T_DCuHgX;^kmf>a>>*#WHp| zHT-v)Z!QdZ0dmpbb*t!AWE}-1Wi>LLr1Z7Xx6OWCaK-r06Dq!=pmg-v(1qGlJ|5V& za)r5e;4MQHK3Ws#KQ+Qs(Ec`B+{>XGx4c2z4y`q0z@LTuebz9FV`;B04>7UQPfF?=>JL%S53gyj1Nz2Xf_k<8zn>sK^k!e_4qlH|GQ!2$k^hQ{L6y`}IqNSapogon<`$ z_>0lcNHJ|JyDMZ|<>kPNrerkpP`OtgWcYSZ7*H?4zX276I_*(u6#F{D8GK;W^(hnnf(w;jCpffG&yzmz#{53s zyi1@MYUL2L)e&`urg_CvH1Oqb$lRw+u8mzkNl zIF&5@DePt~lDxnX*0j|6C`sk5mcO1l!%GE`@EJFvwW}*8`37}p!z$jMlMx$5X@$~L zaNZvg(#SIN*XER$U(Go_MZcS`(z`b;`XEW>9spHR^ZF)*@3#5m<}S_=9@Eal2r7RfM|jk-G_~3kmF7{?DW>zp&p!P;9%eS^X2dZ;h@G zJ1hi^4rXT3%m7U!7V86Q(V$J5(>|t{)Cs%sD*p>usHj0G#KL+YgGSI3M*{8XPotbk zosx0}R^K0N7Ds>H>hYbMgi9MGz2wa^Yw{`J(adIQ5C8e&qyZw<@+Ve&Tr-yMzHprY zaCQRmtZE(5U-s9q<`T`9lf%MVV``Jd9~Ws}jkGD+fkBF?4U+raQfudj+J+$FXHb3l z0m|+(2`$O9xK{dGobZ#HAJebqAFzq;ZZPGKHWSa172B}V zcEJ{txihCq#X79%IaRv7@McjUkx)RL9^yh+1NlWIwm$H;RQqm?jZ?6T2jL$J7oPU9 zP=`6u#w9u1IPD7-@uiPmSKa!HG#!k0%9PyN2uv&Q-?nU~A>K8w9x%do+;|XnCKZSu zx{CJ*mH7;v6G%SkJYQUQ)S`tEzQcbn7QTOY#=hL0=RRc7d9h5L@PjZ29OSPCArver zEHODuL;mTot`5kB6Zi3X;aY`K5~+y68#|M~zN8F|EsHYKNW%oc=vdxA=9H9>D4uJf z>C`FROZtnWMhS>E`)>|d6k@m0!Q{?oi+WPf@{Be9DO*RKI?Obg!r)hjiJ#!6X;TCe znC`1-#Tq^5q|FN@j5XXJaghVoEW2^9HG8c;yq+)6!blj;ag@j}4Vyez1J(Heg z&mA-2*>Ff?Y1Lme9ZiwK*uAconZ{BA)%`W*9&}S4V_la%O&#NTw>f5h9^);7il-pP z@?VKhdrOKqi^ej6^!!zmRCY-Er_;K~MiLBV%Ys*;j%T>tW{6#;*Es+(r5(yCZh;9}x7X3Z@h;z2KPo{4m(Tje`xx6NoU{fl^R* z^I+Np=R7ta>D3blEl`6;x=Ep%UzDhl;f7RKharU6U5~7nyW6dPF zLbSE(`-Sfyx%lDbjH{X{!Rd7-Pq!$S4Af&03J;3R-+)=81-d9iXFpoOdJNx#4GRX= zHv%5(RD?%){;_81x_-_xk68XJt>_gMpdiWnPynrm8UaNyC?~6OP)DfNJALOv+vqUN z^xJf0Ax8-g@M>Q3f$O8mp?GNY-wrvm%8(}x=9ZQfw6JL>H021BJB&y^f)pwYQL)yp z`lN5nY~w3JnbKn|GSby0n%D?aWm2p;;3o^o{^* z+!xZZ71u9WF5jOUp6n&UskQ+B47D=gCcaOpwp10p?>gS#8>bW$5by&_M5KaET~q?t znTrolz70F6>b%KWWcd`Am?K^wFxKoBHskY=@G*f@Wl1s>4(|L7k^(1piHsM`8O405`H-ERZX_AUB&kDAg6ye0gmVul$eeK4e%^OV9tN**; zHl2}wqp#5!saPD{LN+D-dxep+ZVtP!a!-eBKafk}d@NvGFkd6>f&sX01Zw~o*DPjq zbhJEV&V*8%?ZhPPjT~Kk`-f>&vWc>-++~Z49;q?wpj2I~?jLJ<^kIJ-9c%ED&N{MN`1u~+BC8+V5hhntOInzC}>(ofLA z&9eC4eCVLlTX(}@NsrzbZ9_v^e#EZG?dS$lP-cIfaJ7MH*Q>{pXDW>>`}GeSVlu|Z z$I;g(Dms5VIv0J5QmqgWMPkBK5x(?_Sm&oaLtkjP&x$Gi)Y%A!ve~O+9#a#MTX zVa#L{e$Q@zMZ)}s~&S{ zFxy5RV^G`*qjvY}fdLN>CWTwaiCJ9_Q0&OeP~kbv)Pp@kLs@=c=yhPk5ms|^=^(*R z&VGR>8}Aqd1O!0X)*Qq`e94rGM@ev$=veAu<&tZ{Gj=V^@5g^ZCaq<=A)d8cX}WE< z=O^yZG5BoKKtS=#V@O4GzQ0W| z@)3wvEa^X@;JP2n&Z{^eIiApT&Zp=%kf6(Q*G20)l$Vp<4DQhKI@2iU<>dgGcU|u` z9*8o(D?^IY#_x4buJCZS3{K6V-2$I4l{B>BXu17&WcjCj_XeA_t`98?ctZG)M-c!v z*<8KNqS}Bg;r9zn`Tif@?~O-5f|}=eM0ViWp((LjQL47~hl~>-&?XBZyo@16*IRX` zy5f@ZCc)H|F0BI1P5Stmj-Hlmf z;g>Uqy49-SBClZ_8sEK+{c9 z4#;HDb&3@v@cLB$lJDPa492Z3+{R--wPlU{ym9i^K30|U>Pr~FGkyyZ>}vzRLG>8`}t z^6-y5Yp`~NZ0OZIW~njZXJTxgbKOD4#fmeKFlP2tHt-i3lN)?Kjb3czF9OVarTf4j z6%_n?Qbo%CC?-`(m&SDozg|+(9oKIYanBaug!#M*H+mryDP8*_a*i~7wmX0T&{$*W zw0zyO6u9ne<#4JqlKtXZmVo(28NLy@Bdb~MIlwR!=lf-h4873$@=Q>t+O1Y4_-cot^t z%z8`S#|yW=qii-)o@41&$dHqp9XA9xnDBU(@4YCYoiEcBeI_O4iK{qp2C+U1IArnh zVm^54rip$-`WPm5UoD97RhfSu`#6q~fJkZx{SFgCBY%%CAlrBR;u;8<`3yD0Ii})r zlx_EKC9>`QpN;T%Nj6U{aNZT=Oi;ngM1o}HN$XgGA{m+Xe6l=s7-9=HxcA|mBf zks`o>p_cX7fFKmvZ6idm-+{eC^&nzxugV3&*r)8mIorT?>n8g{* z>8VO-98{qJ7_Tv7ojkKr7$<#CPX^Bmef7y;M!RsSq%G=_FiFMCdh5+)B@R!6?+^CJ zhS0kReUgp0+CAUl{r18>=i|`z6Gc5n#IAD=*SbqFR73~*EVkg}o}waDAwC0o>dpZk zfWBm{cl2-)f9wO3LzIMSFZCbmIF9^R#^vW_`5bm&;ak{A*bPafdY5@Mi2ONF4YK{U z5b>Pw&EbJ2Z&E8NYl`Qoh>z)jy>n$v7VDpO{Ku4t$KHv%Ix}WclgmrMC-I6mScm%| zMQ&?9V!?F^>OrDLXv0l0Z~j!FugFPsUD-{dEgPY@?8D-v8(dlOB*4xhlHURX0ZTw) z?os0NZtv`nX*mAYfovf`cfSCzbFmKKt|?00b`oxlftOKxjRfokK`Q#4n4d=!E4Tom z)3e*2Qi+Q3t`z0aU!vfLr_FDs)xV4PxmGGxinTfvsEYzhG}AZ2JNCm0ls2@^dH|wb z8qU;j6W z;6fQxlEK=yV4J-c+QaRiL=2x|;~XA&aKkdMpZT7)K(~U@HvN%zm}PzM2?z0la3-6l zG`?=%Qs#SUw3XNp?ZP<|murXl!djw^H7f8RJI?W4s_%?*99~!PYV~Ss)`VaCcmx+^|UkvqPU%kkpXd%++w#c9 z9`rJK>uZ6u$!`i3|Jg11f%VlHfS6fKi!_o~wSZLIA-2ZWEJMoap?gBDrxDnMzem{u z*g#}$jJbGb>Wx*<74M>6N~(ST@QC65`&ndBs=^_5(zM$PnL9Tfye@1Z5iO)YLmWBo zp3h+u1>Mr52<6O(DokkkOCU@hcgCDkbq&xU9z7^m&zy%NyR}IO2zWLw<_)%k-bbTd zzV{GV<<@88We6PJz>aMGpM{cqa3{3_x{Af zIb9DZY|>@RxH3ya&@bE+$nWUWD{le8wVG!ZfZBu*} zX6Da{sw{qf&)rw^tjr^gf9A$sp?5M$HU@r~Dlg!}YNtdq_ZIYiMWchzEpw45O>K z5B)-Nt8x~&X5$_@;C*}d5Vy>pjPL;|_Xs3%rFqhUwFVgW(mdNv5AJ~Z#&g(px^c_e zFb4L;frnB z41_QS(hqbC2}uZAwg}Z8=+!C14De%Qw*cdnK*o%Pl~c%eWdq2&nWOl42GBl&P-5T! zAcGE7um25LABn+Y6~)a8!Dr#<&{gBm`^CqVZXt1)g^+!qUi-&wvrza`4xB|7bBlY; zb83h@`5+XM{uV!=Gac_xqoSas?3N(6C*}e0`!bNSPY7O(wy52>C?N&>RZ z3wC4Zl6wH9RJqGR%2(3=sx9aIufKMv7*i6HxmWLMiOIU_XnXT5OxNICL($$yPO?iD zarhHFp=a|lU=CmKXdAx%omei1wYcl9x$F`meJ7hWUJ6yv{zd5fUrqbIGKF9d+i#1) z+K^)2l=WGW2qqroGA3(QiR$9*m}F{#58WMn^CXN8!=UEc^7en<=HK~zuY3?bLTH?WIvm}wZL_;q7p;#Wa1}>h*7%u{$7yJBgKM^xV1=H@Mn0cVVM$xY^|L)b=6W?Ub4hn%?y7?c08O<@cA< z%s>_6cvP~^TsbjiMwH?jkniN6w-ngx%fA@xiG^_O;2=L}#$^PTQwu29n%x>?)OVXK zxY}Z5V-M9|y@Gu*tDFt-aNpR&n80)s{cyp)p%ltiaXKb7Kwp5w5c}2c<2>&DeozM! zd>qtt2h=zdNACdh4hg|yFMczSL@~ivY9L71Z9POVMTK$Y@i65p|4JY&#@ zZ*^sbLJ43xgwQ>iLoV?oDLl_y@c)%ikQR1SS55k3z3n&vv~E1PcfD71S}`*<71h=L z`-hc5-{Dd@<`Gb0hg~0QGrBaJvBZy;1ERm%WEo@bx_Szi{Y%+HY6TvDxnk}o%yb`a zrZfv?6*n)mhbfft5uNFU?ycjTy+Vd?Aq%Us%vE&k&9XrTm2CCR*lt%p7bC=5w0JfJ zO;pAmO_}dg0Ffee zzw1T}fQukeoTP`&oIC}j!CqH5`zjd4jX z2GTEa-IkE^4|;9w0h`qdF-BLwiTL05)h)5M6f>@Prn;h#mWrzh*WHP0OAOx6qF=vS zzp!1TusK~?T~3FEX2R!uW1i`cnFUqu?LD)On>Z8QdPhSvbj&loupBA>H=@dpiSd1p zrg==d32gZbBF^-_1jn1tLB?)bM5Q~6Wlulp*m~M9uXE9B;Y(#kti>y z2Ri8s-7Dzt&-;ugRHR-L#6M;%1HAow41F5TG`Ny}c51rg^TfPyP^ZdQ=k|EAb`XP< z`^|#nWC`ZG&$Kt4HjrYD-zY*pPCHa6rn_bC%w<%))x$TOC8@JJ-yg7~Ce_esR)vk# z=b|`I-%w-5d@OlbI2#}3m7@UFinYbv;a84N!X&sX4!isMy@b6KL@ROYFU}0hySy$R8sMA-j7XX-vBPe7St)RJbLl9XY=r4pQ@s zsp$+JKG+lFpO3g@6S>(@dJzK2Iu}}^SfkA`qm!$Jcp(`LdCr8$IR=DI?7LI@wdF?Q z(-=f;dYxs(IEJ}Mg~kc#!Q3OM{SUbZaiUrFPK-PW)+Jabg;KL8V!!X;K;_NDDGujg zP6)t-&Qni9Q>WO^dm??yI_q>=pfQomxBFa8{C(ACo4Vdl({86TdI~N`=tJ*X^QjUg)e~rB$f>xssVXnGHK6 zPPc&ESvaYbbsYtJ1fTVa8VsWZBh`fwzYP{6!;T_DB0LPnO|s4AaBx&to&QTdz*en{ zG}0!pp{fWcg7wHNg$2V{d&1)rZIoy-;AEj%EPR#uICI;99F9Ysm>hoFzn|Vw}`angOpK zPzQ`IC>h%tUtZjLJE<_Ft`kRo7xeR_O96+fR3Tgm5X@!~tlu|jtf8|1`16!AM}%kX z;dku_8n{4=1}O8KFtOn8y=9(dnV~h6<9gki1z8AxG2YHuM1PE|7s%J=L^?}=o&XM&Q8USz6rzhyM@QCzk(*%}`3b;$1nkc;lSRmIk#_{77^O_?<2{GTeGMN0uwO`c4;|yu zvlF@ayE2@oIPDh)nM_GpC2u?U*$YYIeW1Oy5ktT9m%I`bxQ>IH@))bn(f*Xv&F#sh zJeo#gv|A;+1gO14ELO9|qoPsWsmmX4fC54tFz_hdbz;-<_=9AeM;0?Dd8T&Z1B&~Z z9m{VxUXXBKOgbx^RH3ixUwZy`%KP71;=~UX#v?ZDo%NSL@8nv0~IsI(MM<*X&$};kBPtK zxvaVO1i%FItU;CxzWh$lw$XlDfS=jCS2Gn!7A4edaF8UKB+CVeaQrhf&)eLlS8#}m z#{H(bfhCV0?{<*{;o6wL%?lTa8lfJ(oj55k&CQh+s$>VjVeE<1K*W~*ZBJ?0#@gTK zqkrSz7q5R5wVkLBaz-)9N^GLM((i|@I^3i*hut(U_?4EJk~n-Vw=or%*qgtTPPD$B zfvj_+D?a|N7DXUbm?K=WZeL@PiFv0+N}uh*x2nG1x^Q~cyx+FH$I*Imu#{a#ak}4b z=`<8afOIw5&N*5dV0rBA-HNxfwe_I*_;}OM?`H3`ZFg_s?6tzivB#H7a?*1$Z%7-v#{8+zO5PMzmxv*wGdSJebSL4i2?Gu{`|eiPv*1yE|X zi~)znP4CV7?WXGBResp(T!Ru{yP9eG+4H0H_M#L@iR`x1W$rwCoS| zld~Dblflpx?j%2EGZ_Es)RObC=#OxCZ7ozXPp+MJ+_xETScS;zOX}QJG6m-E#@TM# zcVL}50~C6=9@dGYixV&Q_7El^63&P+8>n>$HX!_7`!aT)-Om{DzOX~9WVNY|2gBQ!U6c9dcB0jKK7wJM6b z0>93~0gn6BRQILKD`S4N22n1_{>%KZ*(z24GJ56IJXR{8kem5}Gqy<{|w z?>1k4NU1%!M}!`t#@KmybDyWul49Tel=oqCb3~z;gW<3-d3@>G>ZhzpyIrbIMOn>R+IQkwie+H%pEEIhic>EhdDW7cP@*;rB28F7-a z+f%L$6+USBt2tNo<(XY#`)=0?Bi8x$3Y8Z@VsYt+CBD+R@j5JbYTN$)Tc`2p2LUX&_s1+IkqoZO2f!`-JFZGXW^N1R)6p~djJq%{?ly8yOZW#c zwB-HTboQeZ2&6Bru&-YNZfU%ZS8Sp7B@5>#R``-qCeWjQVW220t5d1JdvJ$wqe(ojc8yI9!cT+!?$_JRMX%A5Jk3teBY9i$Pg5W zv9Eb<_;q$XElbhk&NxX~-A8mI52*+*_e)dL>Pe7qGsk*xNXR2d7r>Q>OV^8@AJv zOml}t*l#96DttvJLBS*6jG@gzr&Y~)t)De85|bfgM}X|nMIAPhm)Cfa?Iurn0PuAi zgBG>U+k}q({=FaGwotycy6OXLfTg@o%0hh&SMqX2h@~kcPQJnghh7(A>vc5wSXfz` zE|!**xWe$Llp?+OA*L|YBPj_xr6#p~*Ud~0e719V3MJ8^aa z(W%%!<8u~oBcFK%%!%G{aLtbH#Ym~wJdgqT*l8?KLbM6hTcu@gQBCtGxim~!5)i8n zOF2B|v#>G;Ndjd}YARk6i+FKbuWf-P{a~L(ak?54#AEVS+%4SY`P1qQIBWZj2EsUw zaPiE3b$t3H5!?1>Eqi9yx6vd6PFMbHt==)Sk(8xqP8X3~!e{B%%tA^r;Y9V~ePCly z;QNDvhN|r+K2s01_;hMP`ov1{H=lq)Aq6v-86t2G9a21cOh-+S~ z@Q!1*w-((cjsZGibzSInsR9o~P+%BOHsqR6u?~LzU4RY?Brij9Vf)4vh{`|f*T1TT zj_$d&4B(6&{0qpZ!96v*g_tWTL9Je%sZi5OzNMu0Yad9+@0G`z@3D8Q-tiUh*7HC# z_{`{c$qu%Ms=6pMDn!U*8nMx3toH|{tlT8{#Ion!VE9|WQ-7Lm80d6dxf;(mI}dN^ z<&~s;B5yX+;+xs7N`o%}&kzcE0kly0gpK=YXv2#G6tK$5!%-NzGbsGlzt8rF+BPbd zB%7c3C6w_Vbo@dleEOV#6r*)tP*WK&+`a~vRa~-Ia z{`cOmn9}<8dINOvlo70I!y~pxiD<3dd2et}<7cj#kwcq_$b6w3%0WV}NFdUIl zP)KSP-*M!<^w*j?F0~9hSiaaFHyeG(&TRU~6ACS{Hw-e<94N4_7FqmNUD|$=okc^k z5-xLVDgQNbFIb9%is{HpX>^#%pCx+f@Yk2@3MUTF%DHcH0Vkg(3z=G@b#X8?F0cN3 zQi`%*VRl6y<6F26yI#YOF%XhE0+_JVIJ6e!GHp+% zlOp2pn8mHgbMcG0vSf6XDTZPEv)H}6hAPkB5npM%29X@OGKd&z4#Xt9d&z{y15Sa9 zW<4ggMe6(*Ln&n!VT(=5yqbcBcS#rxKHu9HqfCtY#D%}nV=*tPHp3iSLh{sgwp>=t$`?g2)`=6O(f=WHLHF&)GfCR0fbzKw1AJZ?5M_9>Pm zA2C`V(7fw8%=tB%7ffkngQi<+^vST?+hdavF1`L5LXW>FR9hpJ#!K7J=v-%%lK*w- zpo}3$)z4RibQ=@){Cnc*c_Udg}@# zGka*Xm41X)8shd?s3N6;Sc#IP>JnmJuiqVcl~SwMb&r(uEVZ!cl2zFTBC5(X9H&6d2z?hl(`| zPE&(MnA3qVn(3n@ROU_MOEz(-7P56~>1Bg%8Qm$=Zj3eZG0BSsTkY(Ah7-G(q1Q4} zKQ-2(Q{0ZmE5O;wHW#e&?IKZDi*_@CCi&u|2Gs2ZplK_n+An>gyAjBes<2#&5u24Y z?6i2C4TE-w(RHRiJ?F}>ob@M8h543Mon)Pgaqc%7BU0mL_g|wBKM?LNgK8db8)Vlf2_^h<@hXpgS+k?Do@WF)`4W&=52iV1 zw^l99BXgmVuZ->zDWVP7F*6K3mWhDzGw(t-eP2~U)n19^9h7tD;0nb!)yUx9Cze3Z zB2-Nv4D{V*V`_4k{dZ3Bc0Ekjh$cXnTyeJGK|FgpHF1pum6rKZx6mgS0hPQg*%NK; zEwNvYv61nzf6;f|8qW%mL!@HcNS1_ceAzS|vscI*+-9Efu_$qQOx_t>{rd1s$|4dO zkjgLs*VMfeZY#iduM^O{{qgiF=2*#rBOjDlG56VDa(FgirSlX)!IhpuCf}{)4a7H9aVmE5Ao(#WWJr0-~h2~?CQ{Gl}Az`RjI@g5b!YRJz zODcFaYIBI(5(B+2pu+R;s%j9}Y`H~Qd@FM-{^7dpbFo_E%r>F<(QiD?-T&WI>2!Ff zI^bHO{t*6X{clwOLv)Fw{KeEr8xTWyv5_7l_UiBcyO`^L=K?4v_umWP2>_{XzxjZ5 zz&?)`ichFv0zW%n%pc3jYLS+H(Ba$MD)4Md9M@u2XZE$XH_EiUZcR;jDnMcGgy#CY z-t+VbWFAP%@^w}!w}kMsii;5~2?#Y$dRYak8Q1sUxqufHdSuN$objaRT!>fD>XWm}Hyv``BBEXWHF^Yu1^p z-`zT}9(~BxdfJA)`4M)$P=*C4%MCNSbu0Bc^DEpR9UX`3R%eH?iy@P$$HwEp2j#&b z07{2CrM5P%-bGNL3cUf~z2TESVdte`NaSE|;KUF`$u$tEhM+R;g!f`A>nN0rxO;HG zsf#qij!U=rhq@wx*$rr7x6ozgR`r4wUrP6!o?a5>@75O9F>7#83wep74{K@~H;X^0 zXg{yOUf&8kcFXwO*!W=k z2167tROpTGc~!V}BwX`ws#Gs;M4g(k!n$zsf$%hlPKuyE?wO_zx5sJnX$=08;1-Kd zy20Yl8j^LZDCh+lTLN7e>E3xyke7kRP5|a00gz?=}GzM z&Vg`3=HJ_SH{>b*COLc8eccVXon>1?&BnGiVxVs_gj={=lNfStm49D%y>VEih!NE} z>SnwChPwu+?bWy)04IoTwlX)*oN`dOFa-hO-*w#1#^N^|zQw=R03`78>@c%g7`P&m zmbD#Ds36#)JB7rDDZ3Rd0n~a(2`!>Qrur@ZXW4cUAmqeVXbbkWMi$4vJGodIJIVAC zGH|o0R{X8GWWQj^SB%RD1mQc0q|mgD88{(18!ol1?rlV7uQ;BC4clco4~s{app7^o z(L%xlcQU~Hqj-2~q7$C3bi--LgW?$%i9O)oNN>9S>iDVKdXC`mNK;#T+i-E(tLk=Br#VxF=i`G~er{P8 zSYP0}49sB2IvU!pcFml|l8m>zJ7w$ZQ!I%|R&Hrzt3oNQlb`nnKg!VZ3prVOjb3q? zv6NMCqYId-S89ZkcTY_*hL10$+^#g@N9;BYR~BWsnh zXjEl^*mu$IqPuTps$=?ruL85Mi)uw%SZNQ^yUV8G_7IdW_#i(YOM1uCOonmEUYrLQYbO1SCu=ovS zApn);^ER8Jd(jp6w_g+crqBomp;Qt3ttafow7hTFE*EcwD3GgwQ}%<`W%%w(Bt{ea zEjWuKd}?3UyLh=~l{B^h<5`i7a2vA_AjKS}dv|Z+f{Ioc^rAh+G9e!#ny{Qa92a6% zfI@!w)~%F74`pM157$)Z`GRvUgB=61LxGx9`*qCE8_9Lp$}jmXBZNW)cIsjM&v)ar zsvhj7UH)7{4AI6f>yGV=qOF5$I)oO#Sl)g_o**X99w0{S+=1U0?m#X`E+|U%Fj0-k zwA(d>UDa12`wF|>+OWq7iTIrVZRlh8LetffgI zVU5_9L}^DOr>F-Ys^u(eGP+{TX)XA!DK$JuI>VeS6rrPb#{O zOTd`g+ar4#F+LW??^sz_grQ*f*TW-uLbc6}{<}K#AdpTL$8vBRIft{+HdPnk3&Q;U zmBji6&MF^l?i6x3!Gfqn;N`nK1^Hi|)o7Ypnd1*s&NQe*?uYHmjanA0sn0E(7XxO% z>|WbL%*d>24-jM>LLRLzTvI^l#Xx7%lWiqf4lck|E@|B-(ROLbao|)PVuZNax#wmy zZuK`k>sQx`<5*Lor*sva-=^=ms2uf^dxBiJX~5@s2? zq>2dHt((NH+;a=Dc-I0jc{Cgy-|&+^BkdOyiIz$!XuKEUYI)f19 z{3f1&^?Z#MhxH^%R*dH>q-;w>j>csUmc04H1|U7JU)`)EHJNEDqTmQSA^zQG019_q!K5>Uhf*tgvu2jIbR$hMDQtxZn5Jd#AtQF@3Kt!?ib@F zOJ4P43N!zXw*59qt<3u%m*W8fjv}ftQX+$>zJR^UlI%}Vf8nX(9$cpSJ>?;_-A>c+ zogjo*^jgdNQTF1r+H&Q}`^B^n5mkE-`~99r32w^TeC3LQI5w#17_tCT=Nr`P$+5$*U~JgqAW=xmfh~LCJ8b;(UDA`&a?@{q{Xx z14KfM3JI)RDEECtN%S4_7vOt)g^$HW;4h|jG!L2UQ#b9eEe-?=mH48BUQq#-@-eK; zRZssKlG2`BBXVzYIf1sEgOFY2Q;DPCa9-rb@y{+?qY*<$x(d22_%F*qq~$9QI6LIy zX^U>=A>WrQuP@Aa<9O4;eK|+LjmMV{Ax*!IMOR<-V79or_9iFqt~Ah-DbQEPmZQ+F z3>_g0n`KeOSV9J6cbQ`N*Y}P#J-;}az%z@y?yIG8l~}{ZE<38hX>6EV*v_pUU%u7X zY-+GtJ-&h9k9a?nF0I%qa4K$k>h9kX5`x=yzxVTGFR8dn?;IP48TKj!JiGr)`aLU6 z``5{*%4yRC*27VlewI0KXU~u0P*a!pDHrWOgd$SjR$^f`)*^}IF=oAYgh21HpKZjz z=}K~HCFB0)C(q0X5Mq%V!;HC8lpGoc>TTT{HuI+;&{~FDW_Kx1NQJt_2S1r+GkTTP z_0d|l_#DIteQ1V5M^ez7o_w9Fay-g?6I5@bApkG5M4u&e?AExqNnmV40q(s%hO{FYhsPBP z5$>z`29eE*{VP3N?@}p(;LI93Ta(ne+?`tQ(XooEN77v6s?p*idD-QAmG2>bN1|=f zGe;~gWgwlprwts!EF^NUDmzl_mUhu@fZW^Lph%%vqB~C)KbPr-RA6%<^k9&U8WB5p z4Hkt(z@Ee*3SJoct1=OPc%b^Ji96G!cygmlM(?T&9R*~O?qGpV|Ido{l_GeI7V6G5f?EVRu(x=eWQ1bM>C9N6eUw<+RoJD_h|r(gdWL9 zN^RWQqB>u+C>r9vyZ96>%zj)DD>qv>t)p9r>kj6|_RT1Qv-%1aj6Xfnm3Z+>Zd|C( zb6EG~p-7pQyzURi6ySD>gueFpkPz0jX`E}yR#GF4(}Z%~jxe4ZCu|JlKoN6%z!sV+ zQV+s<*0eRrIW&gw*5qk1`D4@3nFt0xFzt_}KK28s3qvTruX);k9=kt#1wzejo9r4= ztXS<@yw->RJzkYfswZEYkzV#b9{cy9FuoJhR-3=yS7qdJ@q9D;DcQ$Xzwc!HiToEwoWU0(%pM7FMSFb&gdfj*nNpf?T= z%Q17!Hw+-=gbhBaFC-Nxfve5Ua_r9Tqgh!HgAxCTp;gw@<|Ym3xoremVBh`b`iet| z+|%3piIbQCGWf8UUWNIfe?;ykm~5zS`yyK)FE6h`nWQs3tHeXW9U<6A{?VF1obe8+ z`ehrxoA!NSjoIjbi*|@xbMET^_Y8npg5LI@I>zE(8CBEUX9bcZR@PeBBw87oFnKxivo3CgkdSNYser-19(%?pf2B*IP$vYX zunw=uba;w;z~ti$vZg*`GVbL+-4s5m>gK%z?jt;h5;>2QyGV2#wvVKRwQvE_AQD<- z4ZhxfgqJ312U@Al^*o*&#bDcwd;WbLACzJ73N4>yoSlG~D!Kp!TC$nUEo`zaZe$H^ z?bOOmEiKOQ^DX0c+vxq4D>(!e5f>p97cC7}@=kgT$L)5DrP~~zJqA~|W^t~wpE)2I z85t0fJi!@pslAKqE5Q6}DI_T)LlFM!7fsKHJ!z;`t4pbdEg^iGCNraxDM9VOMn~8A ziiyD&@)cOqAye5$+dFp%3}_-NBAg%9&XkeN%rDG6?=hb2we7^|&?Ckh<)s`zk_rm1 zMjx*ZOkong2FYU`Tq>SMF++Zj|1|t=85OeByZ&UnI={Nc@IfAlQ>Dhq;TJZ}>G^q9 zC=`%H&2WcD+J~0HB6lC^L}79=Ak_HTF=k2~@K!3;7JVI9NM0%9x}_LAfdtGzg(>7^ z|KPNP_gUjEe)5CxJLwOgGtNzB+r3j?aZOc6e;Umu4t>e+C zUSd*tI0JXT=yhu+Mc_fNy||Mskz3h}W8nS~6-1SFDhTH71A9~~I!lgJA0D|kl zEP>}7oa_N)ng5@fF1XQH^QP45CNTZ?>CN>%Q~!LhQQZeq5}P)2Aur<|Q`h^(c}bzA zE3=ZXbbve`Wcl#W4kj!4d7^$3=p}JP8e9I2N6Y#jXuw!!oAqD~u5%)_fRM=VpPQGy z@eQ!2Y~k@!$95R$>+YV9wX~>`?zAE&{t^g-(SCX7?~!OQl=(y~Iv-mxs0EGV=rR3f z-oH37@R)=)8BV5t5mXYmKQD(2J&eleU8E0PqSt%_DAtTR6s{cH*39P5*BW44J;blX z5+*ZP64^6t+#%tE@I9dR0S;lH3}Q?AfX6|9$(o5?JNgR*v@D>(O}$rR>)Zc1gBzC$ z=|dFWu_%-Lqr+})ZYIHD9Ptm4HI9>wlpM?tPoH###M{u8=WP|gK08ax?grQX9hp1V zTU+}EveABlslj97^;D(yzT;{i5w7{%nTkqlQ)^&~{dRQnnvP21DVKJA4L5u!|04ukmU&(sTL0THU< zYK(}wEVq((sL%xlh`T~$)=%!-Wdt>upSSUlp~)5Zyp>_XqR76qcNFZ)q`qI7ZEkn& z+6@%6bxXq37+SXNoXvlbs@<*Iw1Rlv7oI6pCzoPZrH}X>`tyAdVXn07`gDou-h~p* z=2KTLXwy*HOS5c=ry~X%8(~EeOIy3dTndb(Rmt)LJ!#Ztt72kF5{RfFtitsKWfNbb z^CqDY4l=?k3Eqo5pfU>RXo7Uf`;Ftu!}aG7Q7W((y5^5WXa7Rp$DbcorR7&?c<9il z?G7h$Nu->(3A~-$mRvKLUhmC1BN|o2x<-IzZ}#q&0jXx{Hk1w@LqEF})O)mJ3(e^{ zF$2JM`R>N^NJ10j39skO=9HxU5h}3h`nQ4Q`$2k;3M|RzvM0lTfB$9%J#mVUI5*cE zpf^SOu6ylMwVzvlIiWbFD@pSlrWyUhKOfpV1S~Gs#*BZlbpD3YO{W597-g%y)DRwX zKBh2;%F8kBnnLF7hx7#OvP)md?Vi4Za z*PmEj8-it?o@;BD3MIG*l1VpxxiCVI5-UezV@)cvnqT&pkf}4d<0$NhspDKx zzq{-(xT=+kbK$RzjT})8)dG5Ae4grU0`ejjHEfPVDAS5apwCYb#|W&)=3R(Tq~x~` z!R(og>gye*Brh0;qGu(9*&|;1Cm6lMX9)kx?Z_Tl9DNP;3t>V(oSF2#h$ZOY`Eml~ zq*6F!{$1Yf81z$*ADfs~=UAJmR&-OPcjNd*=Qy$E(logCbxIx{r^lmpa-p%!(h4i{ zuc`g|RGo^+a=tlY-V<7AfB9Th5R30xe3VM3b{HaPFp_phcVb{ zzc3`>a94JJN_(<@?*6uGWUQQR=3??BaQW?GgcmmIpv*$tqIy$P$BNBVzh&-d-XH3> zszfCG1f)v5APMuV3@JB#uRCYygs9L^BgB=N#o!r`K8kcKOuXkEpzJ=C!Zpp zN;+QM?OL}u@oy2SNw3Yul9-ik6J4NC&MuIm*ljO3Uad`;Hi4Ub-Ut%P;O1X4!#Zud zeg5osGZE4^@~ggGg>jD50wTm*C+WewYG_$B0Te3V9H+xi{MQNdbKdyb&Spu<7(Sc^ zL&Bg_oEF+m2>dwji$$2S)?9yybn3^AF-s*gLBCLGBRaI{=cSCP0VJDXCUB` zr_*hSI+R~{4ay7lp8_7IhZ*;-O?F30uUn?-cRM>flYY-x_m6vuY1Ir!@n^p-qiNmMD7fR-!2Z<{a;`5L_A$%Vj&kS)1 z#Ir{{%pRP2Nc{lJM?}EEZ~acJDTg{0YCx;cNUKN{KBvg4q@-^y9DpxENtyu~zT_d) z7A{9cm3~sfktcM3p3D65Dh_3bAoVpnA>QO>gSHbSW+Y0lv<-jG%&7Fl#X+IJdO+Vk zMJ$zquK!G0zGBeT(?k3@UfJ<%4Fn2`i-&CT{xF@3vz1I0s>s6oT1I66|M^LUE55AL zy>KPEqUPB&xUF`Gu>xSbEB+P?c*FH-TElMI;`i#Ty}d-x;kCGl z_ZB!+`;c=&aFX6I4lsqTQ`NZ8{f7;_`un#qXtw=EsL%iNEN3C5!llVOoI`Ef!fca| zmQR1~3Zyvg@B9mF+r^#?@#vqU7!yLz^~b@);o|0UM?oJWzEO47JHMzT!+$4Uj+zmL zyv&a@(h~H^B&y9slT=x$_gtLr$2l(08{i35G*;t-;#wp0kf9AcU1wvIw)WO9Uy{gT zJp(*?-uqgp+hj%*J}BbEKWj=>9r{Uv_kt)Uhy zdngv&!uw}boh@?;$p2IA{+YC*9PKr64hyTEXvvX)Og-I}T3xSR0F434%$T*2kP`t0 zw8>0;5}Q0ljTnhadII}>!hATQsH^xrsAJaWY3JG8iKn@6RMr}BYeAnS z0Siv(4ZYCob?~7?=r0M}PQ&%yf@)9R`S%RaBG4&?9VfW8q{9P)ZM2r+)bY)CETa(Z(Oi~&_PxPAnUr#5W+qNhg#KqhUs)HHF33m zBA-Q?(!=K!Ju(L`tG=4$H66$wT^}#7`4g0efm40?C9cw9CqF$1+#7$o;&!2j!Bo(@ zx(Qf5Z9SKLz>e>4mY3_ek`_c1o>Q9ui>}&6f7lO#d9}7N1T?AqvA2zXSm*fzO%nM$ zzdQzA7EMg}Vn|!nXf7HL;nUhkn_?y!`N0IItZ~XV=a2}{pA&_1b93;%(YK$emYQ7#fLkC!A;AsMmEn>KBQ(2 zS$j_d8NS4D8u0o`BlALUZ!xA)P=@S7+fMQ$KSY7ZEnvPuh4+w?;({HN^#Y5hxEIZj zQEM5E1KNvC5PyWfm_8AlakxA&;Qj~PMul>Q`8n)XylAjOz|w*n0f1&C)L(HQXP z0tW=Kd10x<#OU1p*1#S#cqIqLj68*rr=Z!3LF~+mPXq-=sBCI7dwV9`0M}akmgi6@ z506o$cgV`Ol5hII(qmdn9%Uu5$ql~&V3~%RR_9wI`ip?dt?>8!#eQD305Cnh2zj)J z_<>tPo*XQxKxL(=k>Z3ZBsHWp00$k)5gE~a5B=HnG=%XgVeN>_f6C&kCi64lY%$^A z3EPt@AR>aUAWAk+hu^{#fI+#r{t~7E112sufN%!Lr2aE=C8@yzjv!VNz0^)%b%lC6{zl0mBOVld}OqWyZ!yA?Sohumvowv&~S z(&7u@qRjMgYokg@GXtI#Yp}DW)_n;b7%}o;@k-8Qh*D1uKXe3n8`jcd-W;7i(!=n} zYebICPMa_4TMx1iw+)nTBf5EI*fjIn$D1z#+}x@s&MO<{e`mo+{>hhR=PU72Ff1(0 zTZ_KW(P&h7TPHia5dqA1gjGcZdOS^Rxv4m8`}^FXvMwf`A(4Ute0(-~HGP^)jlCMX z=l1Sr@X~Fo{LJZkv6U>SXE0!rOWrvAm7zHvr<0*&fWS|!WZ8YSi~VnLVzIMaja>Zk zSrIHHE1MIJy*8eNVdReln+N`lW~SW93xmFPp*@4H|E}-PU2MDx&A47`zjB-VyJLYM z;g~QZP4@mEnygX?AXgsuU#=#Y~o6g1x1uLG~&g9m?#vh6H5~3LL!*H2L1C z^&~3`^FBbEJPZGtIK(L`qT;7^D6ScmW+n=c>fl?)lJywSo|_vw$7DZH%Ovklv!KRq zd+zZYkZ?3c2F52mVE^5(vs5*R+flfs7)s_cbI$OGmLZ-fpo>Z}L0u8wSC|oW0Lv_r zER~HreH=f^?@E*W!4YXaCJKe_Tr4}be!RPpfA*-$0kkv(|fV(L4{72FB zAoXyMC$9Pzo%-x247!Iyj2w&N^opAm^9zp66(Dm-Q7y@=$L$snJm7j ze&BUZZuC3eAmn~a!Y}Y=?Jn)<^MfDz7xSi?1oaYeNmWD0JD|GQg@BbD6%sfqd=nW3 zBFT)U$oy8~Xd7?&*KmBO#4ql4#{OyT-o^Z%D3G-xItcr0hf(jH?$xix$<4n0*D^Eo zU}C8=yr@Ux0LWgI6ABkouhff}t;n6BuR$^Lc^S`2vgOU#IWpN9?(6L9sW_QkEz%sS z?9bG%|DrsD$g^B?D^#AwauV4D!O!5%dS6P#tuLw#m2&{JiSIm0TX^Q!3aawXW5T_X z8>RYyNm!k6QUD5NJcEZ0czZtvgZ)Sd^%tZ?}Rwj;mV4YVtOt2VO+$SxQ0H% zXpb0l4M(9Fb2XvVx((u!4U@FS>@7W-rw;AoDJCFLB*D|c$Hwyw7Lx%3U8SY3NlUJe zw?DRKEa|T@B^zEBiAtlituPv|e&pBH_%E)_M>444@e)+ROL__=x^W=y75>FfWZ&tO zY?|C@-p|m_lgeHn|Gcm_Jx7(Yil0~7xhBB{X?1p4zI`=6ovZd8*1X3LgO#r4BMCg*1@g83WRkY5Idz@v>t^_G!4>#GigtYr zyyaM0sLIP8<{42Q5*#ycfXe`Ru-2ifV^;3Wt%bey&2?vc)c5DH;ugn_9?ea`8)B-b zFTC87?;SCKc%QKDqCoK^FL3S~ytOE!A@XlLXWmzcI((8sd`8G87g0k3(i9vaxKNM~ zZA7GvUxOgkxbN118n~A%qIKsbVWcW~-2$PgzDk6u%(g_fIQJ9w2+4VaMUo=S`K;cL zhISiL{0&cqWFw3aF#K|MDq&x%aU|knSC>#_&48FQ-bmOy%m0OcKX@R7j{W-oS!N6JeC-#C+P%*qi{KD zZTp|hH|V( zN5Xmn=v~`?V-&8|8sI2i*gJivN5o1Vt(JHkf0*Frl}0J#IL&grTx^jxY1HQ7FZE#E z507dTW;E|y{8$9^9UQ$v{KX&}1AFW7RLIlHBU2#CF_mnm^n`-D*6f?-YUEuU{3QsT zsz_4#z*zBPDxCJ&sx?OsL-zD*_Tr!Pc!?-|r~4oN1ky;+8YznRf>gR&It?d&;v^5e zzqW0q!*RIu$L<<;&VG3lfl@wh-WS5(P}kGV0{JOiOoh99A4OvxoxHIX`xHZR-lk5h z&sFHN7p*S;;sFUWtAhXYNVoPY*Q!6wZtce8_zftA>?cpMOtJ7x4Lq;m@Fq_Sw=pWY z3&m({yriU&Oj`{elG*CXx{ZH$+NTY!9Lue@DCJCXxI}Y#1qI5Kw!aB(Q5tUTMECGbc0IwfOMC14lUBsEdz)!AdRHd z&|UX@zV~@BP}(BRonV9chI(wWW;RH_;Vay-c3awCvxOTo7(z zHaFXY#H>f;WJ|p1^F*68;Y<+K>}PGSngc!?muT@Voz6FXLT2BgBVKQygM))0m*dNH zxLCZREtL-f(GrEn4+*xv?!4N}60Nz)X%*`NRb>v^$eAqR_*DNw5PU(Tj$EVusc5V*3?ICG`wV_# zJX)D8i2+*DT<&>pSNdR-7sVN5(r)Y2HMjvsZQT(l#arv!NnvVC*!L6cmdUqV%I2i^9J(F;nDi|@5Q)M?>kb?WD}xo(%$#p|o5 z3VzF-5BE2Y^VQ?L0N%@G87ZcT_od?idW2`Cj z{67vMTu|(IPT4F#lGWk=I=TfqJfC022&8gafI<~E7|=k8HQV2U7=zVQf2yD5J8=-H zXpoeY6sgxUYySeDsis4q>g%;o@4kbg0&$2M@FBEJ5b@ZXm6braM1*IZAy%GVbh-}+ zu1bo^tf>?p(9B>TTx8wg!icdBEi2Ug;a4EZkK;taB++;ARcm^zm0v!AE&L-WS5fVwHgM+2;A-c>A;J3A@JGF^Ha66gW@Ag{8W)28*!5e1QNE zkjc5<8~28gi>K%p8>&&E&Zawuw2_@`1)2y%t9L(8x>!ZW(Z3FU?v$iX_#hFDOra(P z7rO|vWi&m}R?-3M`1IW#KgQ^{xgOiiDqW4GGO<$C+cyhRo6lSV#=2jc50U=N)w9m8YF%$LF87u~# z<)ozz$~*88d^@F*lJjQoZGA>Rb!2UF0pI%SO$RTI)6x%Hj+tr`s}jxQKQYG>JD3_6 zMal;BdmGso=jXmWOmoLB4B2IY%26;)PMqJr{kHLkB`^W5354lL;$;v$-x%2T7uucj1~ux55p|+Owirfg8%Vy zm_msqZ%F`0Yd{%d-{B3Ece#TV)p6LL%TfSD!EI=IuCCtWT%CI_^T$&&$1mP3sPnDm z1^@$`k5^;+hPyy$xWn+Z&$j~Tmn(XH-@_w6RIAi7)|3KqT!Mr16*efAK@zg+e0Sw- z8I6HQjfY@XD7M^sH~P3fT3kw$^F) zzk3NLwuheazl z&2B~S<@eiwJvaPf@_}YkXT-+SnOyFI+=s~AcSOW!se-P9yQ5t4kc}mO-4^$ol#T<@ zte9meI3d4mXXUW@e}~xqOH)W#ey%((eH;+gr$G%;@XUQ|)&P8Q)y(*^C`;O;K{tT! zDj$rN`uyiC%#b93M038eQde2MFTA=B_!NH>B2BOYmma7`t~}IGS8!IHt2n)2Qp!!+ zucWRNF%evr8W-lrnP!uJG=iGm^&}GlYjf++a0Kh)@qcMNIX_Ao904*IOUO*?-ERhF zkD|(OW(g3Ml?~yuro5ylY;|6<@xje$I2?ibdOR*9xNkn5`@YtyUA(vX+;xMG`<__7 zPgCN}Cbibd)`Q`D=v(drh%5;69k{|!I$yOOA7OQzDW*w)2@k5!uht<*P7j=V*Nu%E zLIPqe=OPF+521;m$-v~mG_Oh#4^CjVz)-|VX9l4VQ2qhMY%yU&H{mA4BdBh7vSEP4 zISUntgs^n`mmIZ7$QzF3hcJGhR}_za)X3{YAO)NVroc6lQi(kZ06;sfSPs@9aUJHU zkWWZI#5Z%6X0{1dt+RzadPnFA#dp?XEiuDre))wvTapXJL%pfLM;A`n%t%K&7bN_6@Fp!IkOtlUsjuG11{1pArP^tna*Q}QdQiU=Tu4HLn6?Q z5RV`zU4|I4)@1+CXtR2I z$%GxSoK{<6%Vs95a7P9dms|cjWDoceA7G(sm*xf3!5D#@j^poOl(OXlCX+{U(n4SX z9Q>e+hO#sx^DqC%F!`-G6EzLD=s}vway#iy=<}9MGhFO?YJbGqe%8SeQkcEj2b<9> zeRNBMW4b^4d!y$sLnis&=W(W~nTeU8kXe2y#dZ|6D3Z60)|pxDfww5jfp}y;{rDze zB2!A(Tsd!T(rAF9X>Ic3&q-){tdwfReh}ei?$zl)+`=~-2lHF1+|^$yR73V^Aty-& z8ho+%)OcVG92rH_6!|YM#NQ9`YcpljnVR$8nJy?2!i#*1NQQ*_wC(ZMS)W0@@o6NU zJ3Q%eX6}8gz@Fd1UZXbqPLs5pjSH#KzzzXF+4~}al@4z%p3&aReR)Nr+~?7W?le+9 zB4rl*QbrZ{)eA?qovCX&laFWZJh>Rdcg_h9WcO(G7@Bm~MPW|}Vza~ti4}qqA|;jZ zp9RZ{{IMS4pI9q#r+RLuvrBRJfmTY0^q_IxLH#L<%G16+<5~Ko`zsqozWEURLH2@u zqV6EywXG%4kjQRwhTqZgoBlm}95?D#40eI#$pN{sW}nR+6?U-(>;clvsmE)y|K_#% z1cd~2T=}clwU7_RwvO497&Q{ADVH`PliNLx?&_satP4Zuyc|?@wy?frf;b8=GV(#x zu}=JCH`vmkcd8{h+B&69(Z_-K&=O2@5WaOoR0a#a+VhymswrtaJ7lz7roA)-#3rI4 zoHd?RJs)2V51L|o^ox`uFgS&j10Jaq^URmIqCh8HV{0nDoFTiE8x$zb%nih49=-B_ zV!yLwXQ!~{BZDwndJ`TU*T>Kcg5FhlW$jTs0rI#laB`i^6~x!R1R6ONovo9S%xMQE zA;dtoSg|2D_$>vP)e=OIEdxiaipD@qBTqd+!@q{Pfe9nH+|W*C;zC!@)nVpihf@iy zPXB?duZr{dD>D)H=`=VWGtIxDYk_*(dDQd9h4bqWqvjW|a97M!O};+cgTqk^Loy@} zp{}oK^XQ3)(u=%l=$tX-6qhE;MI;s$gd;E|vpbm_GY`j7xQdnZw1s4qhp()GNx6J- zk6M?M9Av&gq49q*!T)<@ZgPI&m2DtxtZ!{;kOVvz=>E*O0H?ckbV|MJI`QRi7!z#4ar+;%!M^H&Bb z-z}r%%iM>-hrbV>cAv{oHhd~FMtm1^qyoP0p?~?d*i39;UNk!8cSs!iu6Oy)=V2w_ zZgH)?UNgZeI^Q;R#{Z`CZnS?V6V?!wD8iSPv6Ib?DU^Rs=!Hn68*JoLaP_ z5E!9H#w9$%@f5gVFtz66`}Fl|c2q+o?90{L2;SQ5zkl-)NSGiD7-AvQWrTipP(qEz zAl#>Z8rT}3KJU(l4%$>ntiN`Xu6DA~=aU!DTD zMiypX><4rOWYwLw!&v##;U!C`%fVkOD0XZyURV(%O21!Dtw@=_{_2Mg%Cm|89oC1< z`i*W$Fn{hR|9VOjD`F5=Ap99AD3CT7f+I~>&O}WDhjVZ_?Yt%b_VZ|Mvcy(fSa@`4 zDF<<0{JE7S8b#X)zt^rQ#thVu?hD5P3#+m~lJLvw(|={8JLu^EpLE7v0Rq=I-$O!P zD+py7q&;B-1|vZ>8!07JNo7w!E2%G&#szpyqFb-vjsugS|Aq>K|YL$4ZA7?d#O4J0U-&$ z3#6dYHePWQuNFc2j#JJ>F&H}v`@2CE24fhtI39|vr1uUBf+;L`b9hH=S7ml*Z}z5d zEh5g60GP1wYb0&*51g>8gHGEomUQC34cirO1}ho{(aa0s62;NyGL@1W5iEGp#FyiN zDre{lOsZ$3b=z|AJS5o*KGnxRuMiak7OZO;mVii3f|>8OF3zjhub($Go`k_2M6!|- zFT7>|>!zwIS?|?V62H+)>#)soaA=z8=f@d2K4|gIYw;~$H4Ic$AU!QaSkO=}CVr+YC`@xCcXa8(eop{hx>no`l~ zFIL8_d;f0Z|J3@#t!Mjf-}&|K+

L;cGp9Nq1)h?`2m~)!G^#DOiE9z4nYE_sM*W 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%0Dy0N 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

1aq(+;$% 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

  • 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%0AuU^3ZO>3#WN*?($-KGh?oBaATU4_%B zaY@g_cME`)(1!uXu_tOf`EIojDKL~432(-J4f<|CrnMm<{9-`sLOGamLr$y%s}VP9 zO*QNf3ymRO1n;@uuVhIw_Zy8-v;JG!k-%D>L*frIPBa$Vw3*L?hq{^sq$p4 zV+wn$94SE*D#w!vCnO}(#-&+>VeoZKefoG6}4E$nd zITQQxcw}!uAX>GeU~}7Cs8>j>=8vrE+?N_uED}Rc%8v1;O z!I?K!IO?kEuQ_^ zOhiDfOzlH*yZ1q}mH1e1R;fW*>&42UwX@B(kq)!V@q;td-ta;LFHFkjis{SWTeEu{-<{HqM6u zhbWYTj*g`EMc_q-tU)aXyVcQbcsDXD;4d1X>seK_>znn*JFGV<14OI{iHb~xTWXZF znlK}k(PBx#_+*5kyvD3u8O&}XYhyUIN1O6DN?iBc;-o{Tqh)g?y?MtXvm(vn>ssH|?Ulmo~VAT&Lgqg8`g?x!p#J35kA1Pp>-wrkh zWg7#7CjG4tL_no(;S8+()ysHC6(C%wlBVG22f;!jg>F-~5;2?uoW4pV3*Nx2{|#aIzr7C^647SD zpL+3c&=vi6!Sbv%$Ij$%W|zVKj^U0^Gmj#HeT9$q}# zl5mx~aK(0?p<+>fC=&^V+9%a^p53MQbbe9 zPwjCDOmS>2QrM@x6`!0$*D7y>51@%eY|Q9AHq&msb({IkJUpb1o_y~VcilDP6MOPR1I$D{vCpcW0|7i`R|VeBk;JT ze-5;?>UxHgo;F0`MmVyNXqu>2%9ljQRr&tWqRlC_FX?{crrgT{$m`QPwzF#KH1U-R z1BWgAr;r{`dkYH?e;InZp@L9O|8l0n2q`uFZgE`zTC_m7s#+1O_zvW?h;EFkteLE#}fBJA^6L@&u z$gLAq;5-o8K=#g)re1(iAsVk-SBIRVoQukNo@k3V@;hd}%2ckxg~`YB!N9Y@#Xes8 zFeb!-63^?)IJLXu?1$CBtr63!yP)&IdzO?6qN>h+TRZ{B-jrp(v+q+o-TZA>ovmgI zxE!}V<5+a4!=2_DfQCxBpuaqKGn6Jxhre|!9G1fJ@0+F0dmqm9f_^UyEfiM0Jk1kI zHji45u~XjfCgv;SY|)!n(5`SbC{2nQ$jngO7HAUUoHL*{c~?>D)S6-=Z@t}dm|jRd zFoB$gFE~`FBu5mhYD@8@5BdA;ZGgY{OymJyON}lJw6veQ4psb%`n+1(TUx|kzXn*Va5`X6^ru{VsIcHCUyYF8r`HcR zR~H(Ke~&@2F&`({X}H}^M}+B^Aj_31ieWh}06K}lqu`eoPw~8NFbnLO7geYCEKVm~ zv<<^CTy@dc_I$JP1Ru(XlYCkS0Y5MK`qOLq^Bi9=o3>)gax^0`M+n~Ne)*5my>sh| z^9^?=SkaCR(!Zm_y(=g0Z+CZyD87Q3nm8-Lxx-%nFWU^#I{|z{HOG4FDAVB+QfCCn?=-*8yRqWXIX=`o0y*i42fiM^N zYDRC>)Zw+b6i>mleejpp+g`0glWgoT-mCm=OaacKzVJv{uwA~!jM#Jy&{~`-`mDFf zF10x>DOGDbk0m_P($nv3pA&=%I{L4Kr}5AXmW%N;?hip{n^EhHxQo>6Q1x7Hb-lqr zL&j*7Wjs|zv29|l(U2k&hqV)4mX;79##>EAViFeNz`ZrII8DeIvxZ~u(RR8^%fz+4 zYmp{_th*}4fE!Nvti7aRsvqmc@+QZ5g#46Mu%er{0T%_VVc`t6-Jt{F8ShT>p|{Bv z?|?uWooM9wKoxVzXaK5Tk5EtZO23ft+| z1qd~p8JJ8&EwfGoKlmdrgw4hzTQ-80YG(9DAF+2#n(wdj>CldiF27(x5>_seHJ!4Y z+AigxqLYXO6^ZIUb-Fq_T7nlDhIrWJb=n%8#$|?`#x&+s4exp~G9t)uSVGv+EyJ|> zLomLL#yzuo#&(LeNUhZyf{%m5@xDZ8CZB5mV8Vi_Fg@)?3eqkyPw|Q1s}vNLhN5GS zXG%RTSW5O)b?A#K{pTJhDnm(_LGieB-_wU;2%`LWD&o+w*d^p z=?NJnY&+THBWuC5%V6RpyX0{w*xlDRRrBLzwD&+w;4z`8?)f15_~%m*-C({j4OeY` zwonE>!gnF#*D$V#l!!Pj%gPyA>CA~C#uO#hs7VW5JkRLVvNN3q*XJbfXjnwV zMR%aabw8b+-!Sd8nN)d{{)01rL`2fS-^>(?WZg6?C9T2lf` z2eYUfpjp-puMPI$;y`Gg{pCLS3;3T*?-i!pehXGG!^7nurvFx?DL*8JoUTAU8~s_e zJ56TVdN-e!GbOV(&$q4WHRj|tRNH~CXTufq$LyEXFS_$na^LVL70t2UP?0)=`Kjl6 zQNnxlU4R2T_@IFjUzPm;%?8!pRSi`CV4;gyEq##6x@(it?B*6(y98eEu*hnBh|XMgL!OQDDWGF!pRjz#$*Uj(@d5 zsXnJQ?ak=6;!F(-NT2@I+k6U-iyGpBMCoD%sX#~A#i1}1c%h}f_I2Mtn?79Yjqdw+ z%0o%etCriq|2t>Dc0>ly1N_6P{^yfmC@#z^5K)s_Gwm`fx??B@5XZKTMZKhyBNtlTbKQTALg&as|bto2J0c~6(BPrdP4>k;3IB?WoY&f39U+F)a8 zjDH!y@O`?|i)BtPT5AjpHGj%piG(Uvip+J|va!tNm%-*#8%)vj*i%(A7(h)nD?nx@ zgQ0H(gUiY)T7@T{DhzCjV!0-JRID_-GBch7H4|bFsCcQ#d81oyl&ad)VjTpj5Y1ZW zQ#LjQ0IJTwVf7odx7G>sD5PY!qW61itAU~&l^=}0r>5{<90A4$eGr7CXieptmup-I7FGq_{f9aKQv+|i zAE3#;++&^zHWY__#>2*D7Bh+=%FM6vbjVg7^X8LF3MLkuRyIQJ} z_ttm+{ynJm{daT@KoGaq7V3%)Z@N$ejZ3FCID2#LS_d~KPnL2zn?#n3Bo}wd{BEtN z2)_F7lVyKgpRG_{a`vq{II*btalgyV@bPNrLGQ37^za%@lHAx?Z^SKiQ`Y(6WZLv9 z$o|zV+VfO3qKoOry`R5mf0(%tEdM^eke!)T9$%&V zl8GNh8l8RHHz>YGLh>z742@Q>r5(JtKRp_qTm88AiY^=|VaNJcq89LCr>$8GpiEF& zfuDqgmm-&|=5}_HRWFAR17=bH0AWY0`_1JL1SD$eBGLxe1aItSJm*)=)X9+7%w=$0qwQ0`$n=m>~ES3N8- z_-}pbyfeLb{N}t?;BgaWsQ!sWiJ|r($F)7ZDI)zch{=MIB3(--;QH)uoz1wH@xAbI zCaE~T^z@s5H=u{Eln-VyWWICGLv=sA&fIW=ss5b$8B#L;dtg_mW$js3s3&-8j%h$h?9`&slq+s$ag_Ob=`Gvt*-^*tn%{k(yZKZqh;zA6mSfJy2ZrM5{8+gCh z*guBLW4t8GP1O7#IWR*^z4x}RSkQ<}T@l|J!tPB{Jo06}{L?bm<{SjH@PnAY4#`i% zy5p#mD*6Vg-SmaiL$>sZr?V>31Tjlci1Pvl<|hiZ)SQj!tI5mSR|obE;4w$tSR3%C zJ>q3PehaU%vF?GC6oS^{9-LjK4`v%X$nsf@3bvur?;PJXQ%FIn&2U>j7_3_)25jQ= z%)A2~QHgv7s=t{`0cDGO-Spr@cGreeBx7 z@?Hmrr+u&{zPuJbQswoK5_6NQIXy=(`JzWrU3%{+waTmd)-S;UQ1`>RB7J+DuyD#q zX%^h?PGTnU>I*IJY%G1#`OI?K3g^nXbUfYln7yzeDWlO5J2HFRJAI~o>!;GZ5VKav z)xCL|S%%(Q+69ZWy#E zG@Aa?8Xv7?g(Rfd%aRW6kEDjhumU5L?y7TmgHw1;y^{k07Lrk>V9`IHe~M@0#X*Yj zZZIc2DkGLHORxM5B$D5C&NCs^ix1{p{78BXU75!R5#@tKVO3;_m#(4Li?FJzQ?Q`@ z@>H-S5hDgt5U1_z8yuQcWAF5Vh!KCb)^+$E*=LOGW-aKu#O2qn(lDNa)hAEKofRg? z>8Uy8u~V`=VdLbBsB=ZY!h{iQ9PRtDKQ_&wDEcn}Hq&_LIOBqKgT|g!Ag@i&ABU8$ zv}2OTM~R~SBxrsG+N;1Y1Z z$^TpL^9Mwvk969UPkGF6?DIfWV1N$(A>GA}*0DDm(CK}HIL+s3# z!IILQgg;_|cm~NuTzI=L1sl*Klm^Ijx^C z8T=K?FDI4rI_iI}wkz93M@E>P-R=7t38bZYFG?|h^o7P!J;Ir+#y+)9*Gpydt)nd0 zqXQVK^WP$k?96H1xzu&`!}`%#Md|x|ZAJ5^pmW5gu35fr6#hGd5~o_7M>IwmYtEJn zwhOK@b6gM}Ogr81$4;N&#SD+ZV$Shqt;qSP-Q|+*pY&26t-4UjCZlD&BXSL!ICP6R zQ?*4mz?<*3&9r;vtPjjhK8(rK58tK={F z1Y-X%Kvp_!dRYo;r8*zM|97?%$Jix6!Q+SKaLeZH?bwW{d3~ zT{@Uv?^gv0MZL1lPE+`<;p5!b21HjP9>!(=EthzY-ySYnul6+t?T5KAh+WFs*Rq1l z8~ZJ4-g7l9y@Iz?TB>64y-hbrzbP@c90YZmWAt!~d}y(CCx^)i7}CJnL12q-9j$gR zpZrj%@Z|t8iNoW65~Y7_3e|U~04s2YIR2UY0UW^Q#fVaU*ap5mduXYh$Pc1kX{a3T zV8iJruNC7s>UkaSszSg}^KEBmhn#+Cf1|KeFFm@nvIQHqa>iOvZ#3+@$DI*j2?@rC zNMoC-e+|z}9Yoh3%3-WOv&cuQ4I1W)qwmEuIo6&{9wF;4i68<)8U)}ZW8F^ zSl0A@oQHC6cCpCesx&~f+3=aPi9gw18$bOBHJpZ;WY26BMg2_J##3}DApW_`B zM!#GR?$Pn=>F)LZEV{6%hY6uk3i56CEb6z|!UCl}x#m_LQGWhn|g@~{9;@|Tfg;tSYe z#_8$gmXx(8%Ip`a?&Ck*=mXcaVrx99B*9=~4z_U%%6+Pwqp6LGd{QIO1zfS7%nC%e zke?d7v@Jgh*2s-bWv9#M(CQbKV{o=AES^x-nJ<2}?>ZOpS>}1*@49QUlu9L@UBPDt zc(jXL)kER9(oy%B;)56~5BC>48K!~9TPe{xgX4hr6PiV;M<;dJeUWZA#{z3RKYD`E z_$?;Ix4i@F$72Xkm+N%_0B|t7@}J4WfF>aG>V6(3P2A~ zM)r%$0?^X$n#Ar(eS2lEf_YP2(kP^#)#PS=xb_Mc$13(rvl?6!LbdpX!|5fO7*6XF zPhLLkJai)g8=t|HK*)`7WDGpa;JR!CXx)VP9e>x`xI@eie#>r3=PT&DFbz~dU|`tQ z4AW9lc|7>RFR2sy5_#*+Keo5GY0kV|n$Wg6_nOF0Bu)xP`uq6&fgxfrcHeq#yDA%$ zi-AO&JnIehmkw-Uz#|Ju8S^`bNl6go@ajd&==~RWj`;JN;lXFD+0oDYVW2l0tx8e1 z!j~7sR7w`{I&oP6D*BE#hGA?`~&hdM#oqSv2RWy9L!qz%#Si}4ax!Hvh zj|6XB+djOQx&ivZ%Pp6@k=P~GcdVinmzA&0m*U{fv-{EgvooOCOyCs+cbtFN2Uv&F z4ss-iC48NYgNGe_oYQo5b6ZEJkA{h6W@nITaJJ_X--|H{(KNiUOOGH2g0y*wR3-$ zo+0_LVWjA*_ATZBG15mH-%j$*Zk5fqa1T5j=%5;_kw}YE_58FOl`hmz&Io27j+XHI z$K{*{edb^@p?vZN3N4AqW(EgGL_t%2YSXDg2hP^El9ftqk}09!44NU?PNq6HTEG6f z1PF#xLW({uqu=p8w%JFSm%nY_j+W0VIwISQ=nm|=y2YLdm;3N97r=LBr%%5;xC(L= zh+r-`Rh*yLlhqmv%q?}?Zt)Z)-p{UHk83I(N{H%nC!1dOP%htfqx=9MRCadc&+FY` zpafSI?6UaCELl&l^m_BJ0&*yEaw2ny_=7QSFg`%lKLyOI_RRoGzN4whHf`G@`{or0 zu+F;{C%d(xBz=y4RBf4F((ZD|M$%@g*(Yt^q>35hE54Qq| zwu~A>KS^~}B-7J=ozf;6yM4KkY$eQEtt06nTM4>BA5Tm~mKipyjr1S)3+dUIhSA@aT~cK{n`NjFh}tpZnY4clL^l`~AP7m}RYvS;j^yVjXo< za|OggL{06bzf;Vpliab$c76O0zZg^;3WvX%rM%l+d3}Bm-f?4{=oyd8g9>`Iy?QDr^scq;^%ISbdA?Di?VUox2|wB-=l_OB zwhl6j2N37vk2F${5>|c-$+pyGp45rn1ETZtj^V!2I`jeX%k3TBF9)3u%yppGvWST| zMSq5guXILZZaQqIe>_oH?rT)rH(2%ZPYlMokG;H6@lS^mEmWrRdpR9GlI^f#fiMfg zoZk!0(Pd_6^6PjDdeoITC2dRoYI@Tb^Wg#3&B%@uJIFZMAzHjxLRCdZ?z zHVvEb1U{rnDt=#T6URZv;12f6;l{TC+0d3=V-)^e#ity2pfPWsk*@huJ2xMlFXC<@ z6LM05?70G=f6=t1a+OD$Fzt1IVGUJO92EVyTE(Qq&5Yj(*BH(k@{EFqlr(|tGn`Zy z<8y857f`A=2kZ7zggUHdciXkd;7JdR_Y?9NLF+$DGf=7kgeyh@#HZz6972t?VepEz zbS90h-PM|82=-1)t~ z0uWoAM7h&`Z8K(f2$M}XCYD|o;X*QvsJnl zoAnD1(%h0f|1>2@rONTnEsAnN;qTIUSpTAX22Sx}l;vpThP(UA{?7|A0PULzch$tr z!e-viNx$btGm^1uvHBi99bD92YayRQyU?3_*0NSc3H|{8PLd*Qob6_SmuDn z_^);wMoMo5vF4|@mwpA_c%4P`?cx=;CxYWGpM5H-Rm<|gTIhowqfXc@+$~9Si+=>a zrgDH4=C{Tnd~|&1ZG~hjk~}Ct?i6X)6U#{BnI}#Eu6QBwdoh0A-hG<_b!Mr^eqUkf zs1H@L* zCvACNniD_bv)PYQQrHbg9!Hqqqm7*eieLgZINmC`_J+bIAf&+vy5p+L{#JQ?RD4) z`{hHgytdZhV@90dN6@L)N|7qoFn%P8yLCRudC0d`hp^?H6#&Dur^}28)qStC z9+8q7U{a!*ay4fXDlh=;*{xZ$VJiC>4b>6Ek?}43XJTXoO(Gsp7<1wbr2)mcqQh0@ zJeLkW7Vdq9**&i(g^VtR#c}L-QH2?(h(zLL47w?+#K;MHtQKpi_D542y!fe{E71eh zro)Iv=lKqoSRW-F(yjDdr>iEo(2b8a5#gC>MUSuEgh~CO@&6VtbNVty+%DDla|$wN z0HZlg2ZMX)OTIY-yQZ5gPX2e4=IqfmVdoK{)Q!i(lwA*d(52cfqVtwA=yveF4`{3d zQZm12TmsIe?#3U8>wef*%x*dZW!Pb{dmJ*BQdTqE1vQS-?ATt`+QQ%u#t;(#z4B&; zC3k>W@kj0pur-(N3|(p;5ih&qL2ik3kmnGxFS@9OI!xCHuYgG5xkzh*$GgVzU9mG1 zc+M^1Z4?!8<03yalh$c!dXWSn?epN~%wb*EY*0bb7h#s}md{eEi-L#Zp<%k2Q6lsb z&^}uE=ERLYh3BCOvl`!1$q>JrB6zZewdjR=IVeu(cSN{KZVOMZ|M&8l`V>qjIEktC zt+LEV+voT`G6>Cnk5s$8%9ik7?{{mxDlonN5_GwG*A7If^Nh;}y6LPqu^DZ>$ezVw zTCK?lukHW$`q9|;_+`mlwKK<60xM@JeeuS=msoV!GudLw_EaOBF zr%*!F6cjm8wQd)cOxw#OZ@=#r_o#GoGUa5EGJJ?G{ip#8yxu>~HGD1V%N1V$;s@hP zveU66ty)`dEIS9mI(%++R{3a?=B67 zFxBYlkkp9GQEbT#Xr3;Ne$c+_{iyP2@oEeP4S5__BB>I%Qpcg9v^~|>$OtqsvCdo4 zm=60yI-l{*N%Qe^D%I6cofhcoM6Y9*i)1wrcP7{HbVkoqg%j24r#JW_W2S4l*#1@d z4C-ZfRizMOw!yIUImoHeu)|ZhOWlo@o=fNa&ylo_2Q2ulx?+%*Eq?G)?@x|fpH!;`wP^;{TAJcVJ2^j z9SnDEsU9oTzvl`za1wwhOg0ee9JE-^y-K}0JNr;1N>(RUfK$C7bk-df$YO&UiGn?r zFXso1HA7xT_5;<^U*2MM*{7)@>JPweO>o{2bap zS@swmRK4%Mv=Zc6#bZr7sOi*h68b3k)95d_qhhL3&p?JbcgAv5Ts&`Fy&YEHUNM+I z3G2OmZD}jcq2jW-r#0&;h|xF50tM0HWGY4IrzSK$%&lJ&wLURxp&k0(8Wyq%>LTH$G{C%6f^q}Y5K!kS7U4OWG|2f?NtXZH;WelwYHFEG|KH8cQ#q>sZ zVqhSi4GPo09u}37^iA?uNixwnTyEA*YX$Lat_4Zwk2^gQ3(l_9^sDT=-mc7Tv*_G0j}Au@KT}O++B)hZ4FU?=zDss zvoS-G2+}w_w_#DAq{lpnr*R)E>=jKa8c^U9S;deHHW;z3ryi-9vW|eaoF|OCSLFKF ztF%(fOSQARv>g+(G{G&eFYUd(y(zq{HXSt#r~q0As@SsG>AqbvZ(!%}D>Srs*;R2_ zU^Ib!T)A7|lI#74z1dfgPQMEPB-wI88F}=wLElX=Jck`TGmJEzYq`0)N@Y>J5~+j7 z${SROVeELlWwF)<8*ta^I-JPpP^wfSX5To_g^fz@pEr?SiNuI{ED&dD&UqS~yCObZ zEO`Sh&VT=Mf;>^A$UNhmYx_5h^pUfrx-Wx6o%@mN-`st@Du48D4Wmr`kGfFN{YVpl zeFeE5b}+T+&d(Q!ij!T=m8G|)r8Rnor8QSi(Bkp%$XiWM5BBfKT;nuhswm-C^v%bq zNVs^2yk;*l{h$j8Y%vA`Jw3l`@)L9mr%+-@HsWn&c#p$E9G!&>A>u}Kt*vm(;+bXR zcaQxm|2~J5>4LFEf5Z$4IvpM#H;;Bzej*Ko9R>_Xz3RA})>ie<>G(vKKLc-bqJcMQ zZLjSa>6Nfj$*GzolF@y@1xpK7Ab6~Qee~b)sz5mD2GemR$HAA2w`P$-dNg1~Ee+A+ zK-(mJg>QeGX89ER5e&6Y{!29oL(ORiO5o0p8^_|oP9dlS&Ux|poOG1-hejmD z%eUe7ofm$cRbg|cK~wl8#?X*HK)Mg49USbu-a}_i=JVsOt@k#jx_$WYa;MFR9`oPP z`VyZjxK{t0hBRMQWN7IA^?6k%H(1WwnNYs$+i#$(OMHxnC%lFp)xz2n(5e5`|1Nq? zX9Tby%p9yQxv(a!-kz@(3rR@$`U%{1kOV2q@$`x$1wx#qu7(({f8ut!umnZ1w@kQf z7}dJG@A0eRkd{tsW@G*(!2DAkfu%C76->Ytg7r)~7-u@O?DwsH&^;v?FBaV|HPe&9 zn3apk-FwB>EqBgGFL{KB`sJ!A=Nn>n^a{>G#e_$~+#|WHmc+vYPcAouZcXq1CS9%q zF{bV8d%~c9TS9bSvrFhdsEo83_j+{sE;lw;R|^GP(_WzI&?a0;;j2EU6H{_k+$MBFzrWoLF>qRo_@30yUMqFpzC_+4ssf_r zRn@p>WJwYtXt3(E1jVFG#d-C$Tk7Ml0~0d_+_V`5x6w--Hrq4qshYbtS<+MUe4 z?FHa%h^J1~^Cl7fZT~&BEG?A`A5@TmU4OEWOkPL(RX>`oJWT&dA&j7+Kp}G+`q_59 ze&I@}{Jo4Kz(xes=zqW!mz8+_B(LnnA8u77F$-8`KcD4wM2567k(%-@4p;=z zn~_PS+C$ba_XOWnP>5gJ$il%1oxh8qdT;+lyq7Elf5-{z&ybcyi5S3N4g(_1s2BDQ!upX@ut0+Q`ukU`AO42abS$&F31=W@4 zg{P4Y@j8#%C%hk-_f+YmPgyIL*{n3lfX9=;lxQ$uc(rfO%m9?~rU1xXaU0VgXQ`J) zAf;cxqT?WcWn;Wg#kTzLV|{8~i`ik?IN#kig0a=axX~dr{IWE4q~H7*!^3j3zVce` z?)0H#J2%wI)#rTZ7r1598wy!+xkM2zB8tg;iUc;$J)+1UZ~4?z&hPJ2kUdCJ_4#Q! zQ}#!G$g@I`x6^-S3YuNS(g^If^UTstD_!dp2Cb(FWL7*Lxgy?9rBIun6!Sifd(_I9 zG|An~SC=k_FJ!_etNj~eR(}N!|NX(C!Z~3rrS)*uFUp6rVAf6e3C6U6$5LMn!bv>F za+=(NcBuj7D5OCX`EI-VA5*S6ACB!iOy4Q*38)A>+04k3*^Y@la_{`=fxDIvJN=on^@gfL1a46E)L3ns6MvHn3WKub zKaYGJyXQoW!W$zq?j1^8H;F_}+uAYx7%R(?a3a6si;r?{gaauqGt!nx-J`JhEPM&~ z4P1d7sgRF39X(y@2aZ8gMGvMM$9$!x5nfJAGhYrnjn*HUoE77*n_~GjhH5zwDK#w}{RU$>whh+txkV-SOBi$Vi zAi~f?cXvqb=iT4lEPgU97K`Dz@9R3x<0x>iu@WQScO*SFc*fG7#9CP(zCKR|gU9K9 z{V5o2v}cKyZdfex)EwoBN`^nnq>xaq)%rldUay67uHrQ+o)7-8|q5O2y#?}Y7m zZGAI~1eV?*jN`CYVP?^(4?%>nDkJlC6Q^PRW{*}uhA^gs`ZHA9?Vf4>X~|K!>=WX1 z0-A%@j+%!K^%nVH1+{#5;um%X;fYyKKF7L7V_QC#?WU2tnQkY){bBjEdb6vJ%li)Y zSHW}+$*dn}su+MQ9FCS#2F}Q*Uk$l0lvaJ3?GK4}-A=$ON*;5U`g5u-DS7s0N*F(O zQZ+b4f=ZLg+;l%M8cxeLP+^!DmOb{LD@h*+b_^39!DumxwWo$MzuR7bu&663m^3aguYFrzqNCjN|4d5O5Y@nKh;wjh`P}*|lXtH}5C~e$QXl|E5`d**)I_T+3Dc z#KWc2npEhNsqfBK3`P#yT4zayTxRoLO1+e?#<;LQY0dj6IEX8{-3t7qxR#{d(U0k8>0Hp29hdw3L3d1Rh@04Ck@ghBTX*nSQ{5 z5>qROGb{vR@UpwV zsC0aztHz())}l^~54h@nU%@It)V+=hd!g%M86uv3N_?Yn-eLG|r1wW5Wi9FQ596{B znJjcGV>H~%v;yPG&mWT$MEW|n)^?BfGoY_IoN@H+=ntA4B+{A#KcbPo@9f4>5W)~X&*asjlUAF+TY!Uzy9 zVI%76<5=;Vai8nr0Zce~6)zf`ge)m>zAE<75nyliil^O3>AE!cA1CwmkbdAIlDl#e zfkb*PT-5#UIT|3J1hl)9GUEU5vmM`zDQVZ^!?`fhK{lBPcd z^_SW>_))|4@VAaYWubld>%(BeM#(ezQ1gvFQ zwP%A5F+lYrQFc!!rXBm5wTFpCyW<|PR4&CSSiN2TSvgTvyPSDAxWHG_9PQXEnBtd;o=HXnfkd-O*`@>cc&M?{m;wHby4wt zqd(L*d_^Lk}0&^sF#-LUn7K$hMA-svYKfEy)ns0PoG?avXth z;YusEKDb{Y{k5?DU@g6={yz;(iD}n`6I+a;rThztYM;5HRDuy23cR*uTH~f70;(#9) zB?sjx^!(4;?9HGi?bLoZ(%buq?mg?^z~AM)S_5~apPyfgcyk0WE1B7YuT&PMoQ?y@ zii$YU3k8*Xv{!HI-g0*psiq(|FB^Hg$1Vq?7WVZ->sG0JW&6_d0)gG8Lg$dQ*L z=8uNn(}>C0=ABl*w9{ka;wK< zMPkZk=MuhsHte|~I77m1%hieGqkp@{)UapSr-T(Y+KP60Vfj?y!sn%ocTRedYa7Fv z5b9UU6M0tbF71EbXP%ffpWc9RHb^?O!ExpZLk z;bU?EiNMs#cUlsq<#cjv85}E!8fJ%u2Qm#vxvX4lZQ=1Ipk{%fdb$Q@Vc6V^ea#FV zMc67Mopjh~i;bmJ1}xRc4qeHeQS>9rmF3Py`ximDgLtVgOKHi4|G8Ftd^`pnqqJ7n z{xf+6*VCm0S6j9*s&SXc06a2%rngtN7|2Hd4X%78H|=iBnpmXRb!@U+2Y9PAWmB|m zyS~VIOb?-J_6Y^-wqcD`*dX~t`61@_w8kh{&67lG^$^ofi4N_elG55;t`AEKt-eOP z3$BBazwoBTU-!B<&m`QehR18+99pX>-h4U2vfi=%ujKFdhG%wWaKmrJO^_yLoqf$( zKl0-gcK~x@PdN5x+upvKmXg(Q9KlsSs(}u=X^>5E7`PuXmL6XU!qBAG31(|M&u6?D z7z?-0*D;wgMpv5q9ItnWEWX|59%Ve=MMc#i=GS_qfPmCc>BK^TlCA6Pq3?kndON=# z2uiQFm55MP-0rbUn^HmQ#H8y^oVDXopZTQA`vLq&>)o-F*9X4L&G`&Z9tQfBdyt0~M_yX}N);4AKi&Cl~3hO)eFZ-PEU9H(}c2x}VLJ7vFcr%y8}B%-{Sb2|Tls z7YVqCy&(axsjmR2zB<`Xz+c%}-_3oVy5aK6LNcvS#gDv_tEJ76wdsaqO|y=)6KxYS z5>uA(jM52)hR86@A6a;sJVD9P%Is|>PT9zWCffug=iSJYcA9a-=i`)R6QwFM?AS(+ z!Z!)0$am*SqC+Nh5wd;gr;qZo$YV=aiuK})C$koAxmblzO$ly+(K$H`8Q2JL;l^&Af&&xs^O5S9c*-N;YB#`V+sjCUKSS68T=q? z#=+^_s`DSx$Bzus$;x=d)-z&6?_fG@MLUXqa&w&H;`|2Z64i;<8nV(;1bGzOQjK(35ps!7)3EJS=61MX|TK}#{O?e~7`j3@j9FO5} zq`&l;VJOoadA~J~;ES@t{5;~V;oIMl+o1FhC)egDO{8)x^JT*P4?dQ!^e_7+207XO zbevXOb6~@VvhY@rs4P6WJl;3W#5psW_gb41{l6J@2#gVXV;6S z#6INY+TMy;*v-mwaItblEdhq}DQ$*Tt==o_q}mBqhhz@RL81@jQhbHXI7N-TU)K); zyYpR+??(QA7C`FX4~MxU{*B2jw=xS!)kK|4e`a-wd+RDI-mT_)kz5?mbVCIeJi(^7U=-lsdxwR_1%)MKA^7a7hV-@Qf{3@0IftZg_tW1Yh-UOJ12e85bg;E)2;fn+Hb&Nk1!-1x{^ zD3&(qiqv-2veiEh$^sbnC?uDd94eT0JI`j%Qu0S)i0J+kzHMX}z2Pv3dJu{CBr3~| zcVA6?1O<>ubbgiSkv6`<7s!A0OhC7x>`N&oVxDx&b6jL^?f7TMp_HH3;d034;X>^@ zk+kqPS1%-P9q3}NV?Iz?vC#@}+_Rc}TvabTT*j8?Vf5SG^?M)_I>J_9N=whS^o+ad zAQ}i6TsktQz+h|B?B~H`lEGLw9cB#J#(Go1+apJ^;b5Kt!6~&w&vf_CUS$lR)zwBq z=hpAvqJBRU$($Q)^;+Gy*Jo%s1*WKOfd`|3fyqzeHnMM?Jgmn*?28;qCb7m0`>B-} z(cy|XwqGq=4$ifo+U4F~JNZ3~iTGcI9KekhyzYP%)Wl+06kN%%);RxzHGCs&?(xgl z8;yz!Rr*JH*;HybHXBPTC3nL3R0+t7L#fLJ>3h?~b6@G3SJKz@0WGQ*qxaX+roqGw z26GR0neE=IlhKTRi^}1@moPYu53%OTgQl}>QEirGMMm>Nn<_@XE}r`gB6kcNgq_+? z9fg8=A`|%ep0RzUv?AM9R_~$W<{}WWR-e%k*Jl2Z(1~ZyWT}VQtS@oQDliyvGvyW7 z0z6=VM}{<4Y|iwE!61m(hky+KxNcYKdQko6xz4M%Xi1UiN*&9LVTj{$`3rMGk%s@h zV)w!W^*8=r{$V-Osr{Nn>iYD6z+i|oN-Pc6_fasdCi4c|OxZ(4s%AwkCtnWP?`sZCBc4AlUjXI*mn2~m;l z2@Znm>B_!<$Q7lW(#3MFXQ3O@VMawd(Xi?WR@-z#Wg*>N!p3CU;Fa2EU7Up>Ay~ZI z-^FVGMzvLG3z?RfM+-Cp#h6Z_gyhB`a^)oxDqVw>3Tfc1OcXRJ<=jHb?duv(`Q|W> z{D3K<`i%ro22yTe)$b@PjVHo_3@To`!X ze3z%uR0A9P%oL7v+-zf=mfMb{gZ_&^1xui81HOwuAXe^&7f<>d1_89sXruprEzdWt zgN4>Pfaav5bvhV$bjn%CqJaceVRnenNgMmrxA z!0+a2|3_GtpSN9f&{{ztlHIGJ-rgff|NR$uvaOf~RrR0_HcX4l)6e z=tfQ;M~oUuTMT%`hO`zSU#zFCZisL<#rPvwp#X19WGQ?>@pvhuYxt%u{c!^KS4!h6 zhUBXV^Kjgj$6qY;mnJ5PO8MR@gA}IGZ0!6LWc-AL&Y)z5GmytjT-`L&hj>#_%IHOp zOT}etrQUrViZbfUKdwJ|EvfCvnT@i|i57PpmuohKr=teoVVWrI`BvlpTtadFc-VseCO~fqr=8eV1X*3};@VNI0`VL4O#434% z{+A*J%B{p(Al`N98-j@3xs+qoK#{57U(=Z87O9Q+5d2Xakn}kk0 zRw=6#>Gf=;8uTwks_blpwSBh*>nzwxe2E(m1ecNMIpOFdWuDd) zw}8Xnhp_6y|Aa`osPXY}qurSqY%=ULHb-8qpq6fWx*DrorgQpgTRfosuJq}o&Sri! zYoieErB%iFM6bW9t zh&7(uzc_8b6Ze13_-{%6C2&>M8E&fle2%i!R&pVAi_=lsrV0v)9QghNWc{a&BM9ZA z3^w;Zt(dXD*k8QIhunWPzxrylTVY(Ul^1>9*CMR?e$CwP&LaWc;#~sI40sjkIwHCW z7-EZTGOks?{Or08tehM$kTaW`w4}H)r_g>2uuz9MTf1Cp6xc(acK9_qUm6j093Mv} z&oKdGpCo>P(-MzKSZSaAkaq?eW4sw#r48XD5faot`wSnS!9RQ+t*Whr8-Byd=5~0~ zc6jr1`=-rWc$D-lryO{hW&(<$FGImoTn`tTrJ5>Qj=Bg7Q-HVBfC-LFJQ?czmD%^l z)Qh`nVCrZ?B^y^Td){+(X;Sug0*`A=0zSRn3^5SbJ4ZqaOjc?c|vk-W?Wyq9G0T;m}q}_s3i&4cl zd|PLak&3qqjnId!jeTzcX(imn)`xp_`iF~z7m-Zgtbe``{Hwtm<#;ZGwN>=~22<+Z z{2}%slCSKuo`ItVMH5c(vzCi)fTNLnTft*Vyk;$^wsQMpy*EEN3NxzxsH@&YO2Td= zC1UW_*(uN|K%in&H}+S{F>LYbGGHt1S^87Dfm6@9QbV6>f1bPP_`4fU9)LG}Z5?n6 zyyc`%7ay|K@BXqspaZX~7}r{JuZ*S7udepYuk9ZC)bCo`ql*@^+U9J{Uz?|%Nd|7H z2j1_Cmmd+SUOML#UKPB~DYvW!oR{A0Y`d>GpHxeL6Vue#HhzQ_-}VZYz7 z45;4ux4&*%lYTc4e~$|V@e!%G;mF$@{eXTXgOs0+-M(I0_Vu zX#IsESs?g3aXrB7Pd}W(p8DS@Vn@}6lw-l1d-6%~si88ZPj=gky@FOl!#7}%o#j?> zR|~W+oBumM_}|qF=GJo-!c8kob#4y!_rI!r@s3OrJn!T(9fsMwbj)^6ppSUm0GDB< zjrNq}=g_AD*80Zb8B`s*ulNuJOVJ$_lRJ)uXIv4kqGbGRC`zBK8MATviOEECv+@-e zXCcr<(Xtgr%l&NdP2TJsc(0xn8POTxrqoa{p=1@w7sQI`QbA6>uc<1Hoi3iP=<7y1 z@QR|Ep;H5`R4Qt%tIi3Si+{lYC2lAL0UQ4=DWtoo-bwnBF}SwZfS2T_2Sv1)#MxN5o4ODdnJ-E* z#UJfH>JQCM^lsovX#Qfq)GfnqOv%m$MFh!(eYr%7XG2y)dr8jXzSoDJ~(d14qOka~uoSqzQvrPDzBO{SdL@Ko;l2UnzFO}VI*DiY_6<5Aq}rl1&c-L z87m8xBnp1mVabET#=*a$WZ-bVsTH5FFD?v!LE{^FaMkfM|MuTRbJ!)T3Jb7L>gD)W zek%3Cw?iX`2gH^~JjIQ?3|87kY(tAyf7!^OLZ`ic+YA8~)Y_bE1d839OlZjsoq4oi zre$W1edr6^j1MQwE#H6omOSlY$Ohx!1d#~FJz^XYR&is|7IZK!+e8*I#p$LDFF7)E z(oL|aeui*A<8D^D*B>aZofmf)o4~oRnFI!;Jrr8qq;7Css~!ykA2}GB;H2;O~xek{J2( z#{C1t$loX6*{7Edc@e75=JoGxO3#;PsH2y)5jUlhn4s9wHZ*nk(jeMxbD~DY^gJFG z`Da8_5$u|(5$IunS}@Ho$FCNZiSr2sY0$cIhimfTWM_&ZX4c5KR?!YG>Pk!%HLp(Kps`q3bGJ#_0n+t;Q2C@ zVkMu4pzJ{P=+C1bhgi0_J}$_uke8hy}d4^<9x$gg$+}s!DVKh3ifxLrgL>D zhXo{kc^OE43-q)Pbq0BUe>**iYGSMKvUTyMDD`#yW4D(cEMQZp!NJ4g2=N)M-k_zP7yh2VzRBj;k?QWs3oPWb9XoH2B0{A}z0eeKl# zZc8EK{W4>dK~?*ycss&|c4I#CKc9+b_cg(1y@-e}ph<^VsOIILA3xd6{QLiTfjSi^ z$wJ8RaD<`y%%nA}&(%Z`csyy!mQHR~3!{)&Jg3XcNRGmf02JS+p`G;vC=_-pMA&f( zwouRdE%3V8VsisPN_cB?{wrD3vmZtk!=h&m<5x;NF@HRrp*Un#PJz#}F4aaOwr(v| z6*5)j1wk^QolnwmF({r@TDzBdUIuWqcz_DUWHvrJvj{^w$oi%QQC=s1KDCYeTWR$Z zv6y@dXCSr^`^u%B{b*RfjOxWF8X7vlMix>K)%*&sNdL<+B9Cj^&F>U3cW%G?Kv?zU*Z1E^H4Tfo&7SZsaA2f5PL@!f7*6fmU=^S zFlDspy_qe83odZ5Mgo^<%8^o!<>dY+x_4;kqDQ}Hv#KI72{CeKVZp362L||$O)#e& z#mO37obxA}3?&~`n{aC@0ajpnG9rN|WZKLsKUwiWp+!R)I3UE2X-`^1VMbhNg5p!` zp97LRAB75uCOXW@#O1b4ZR*n{%AC%FL2WWzeL5lgb>lEdQ(b3Vo3U~~1iC1}?9bY!AKQrffE(G6TL36HdeI=*1w z{$5I8&c@@?nbzx|QJh&TDnj90C_Niya6xj3u_iGPN0yiriIsn$SrD;^#LLT=XA3!67*y3qO24xx6#*_dtGY$HccW01K@#v&!FMFZ`t=BBqS7~Py?0=SIp zmL4A}LRN+a^t_}~&XBPKz(zk$F~WPJQ?9&NY3 zimcPV;pgy&0qwV!lwAj)MDuM~%P*b&qyoKo2l1oqzYuTsQivb%rA^D<=(;6{eiT9E zjY1yRB)fDfm^upy=(k60E)Vb~D!q#=aL9^)mHmStG+#SpvT4r=au)tZ!4t!>BF|P< z@^+h0pEl!$>#Wt5OUEx;VUp0()QFac5XPOG{W@xaX7B1z0<3nQL)hf2dcZ|AK_3 zUXAE0*YB%4-op-Wwr-?Oj$2z>(;VCNS)q-}zMtlnb8mtf_t2nXr1g{H`pb({n}>_j z!<#bctD*MuDf8J2|7N6KlS-qpIYZ0U560{G2MR~Wi$bo9BJ(QhoBI3nz?;PxpKipR z7Nwf){71QMm1ceEV|J;F{)dllH?8#Adeac}uN&($^zG4geBjD;;J=&uBmp3vps%!@ zo;m*_AAWQiaFO=V8@R82OZb|`*8yRg`_v-|nC$9FwfJ8Gg|Yk7WIv6LOS8b&g&&7@ zYHf^2pVR{5GOu@1tqsnX<`3tK8dp!+j=ok-KQs4^)><&BJSi8lbffg>sQ1U#Kw~(O zH$QbFMTdDSfse{`T?`mk7^ezSfW2qs9OI7-ad&b73eAg*)0SN^J%^v^h7lZ=It&MO zUuR@pWyf5a|L+YHSd(6y&rD};o<7Zzzl#YTS~_rii_!Q}ueaoyx3xjd0c=xcLLs9@ zDJMVJNCHSL7}Tk@U^A)Jc;s{U$N%wMfxC1 zC9(8~&1o&lV7+XLv-^RIjpUgG`&d|P%#dZ)-q-GP-I24BGJ#sUMM14$v$z7uSzJfu zOydbhR8)~Kk@K;NW$_DGMQ}J9lj&d!x)STl@(WS-j&p_JYDt-13nemrV>?w&7*%bO zT<_WdZ!aH)v~1-&lxnNq+|xI?OwXC4#9a$Zsz?NuooNS3W4B&BK@6UK4a?ZAv4pZR zc2KIvdijkx-mX6N=6;~vFSW`cXt!Qvk86KPe`l)`RE5gP$Z3YUV9Um0ca2X;`idXu zw}K@!b90<6rzPJ6NDWuL4YbaBtIgamm(Qg8RnLIFrHwvO(+&EegqrrT!i&}2?YJTV zb&+-PUnI0Dk7!s<7;sg7vTOS&g7YHUcAMU)(+;b#Cn?Qaeblzp$SA_Elp*-a$;@~> zIp}23#0qj@mHSOc_O!gK0uINEj2bES!M~R-CblUGb}7?VI?}li|G7Hhp>fq{ zb$2zDK0X^ku|kTv!g%eO5{-`#?dw~6VdYZ%=jA&QkGd(Zo3#&TG;%{L!VYIS7wQwE zFjx}l*pZ@G>h2?yxweiTyg88{qJBp_$Fm&=6I*SxP)s-b4ZRgTg7M#?mm|EU(P=CU zMaHMamnrN`%$pL0l0{d~nH9T-NOedUd%ky(7m+YT>g9gdNPI<15lc-te!~CahlALI zAO{fUwc|&~7R^L3EoWwHF)Mk>uFxI3%jybs`yif9;X>5LY#!mGWkD*DaylrYkiUXo za!ww}66FVLtl4PUzMvjB$dE7*g}t|O4PEvq_{;LP_lu25#OI{$xbdEUycK191U2iu zS=n+&QF&{5VmoGin3!ZE;7E?^6eWhQbm7Q58XWD=t7HeLe_y&hjN5nCXNR$z*;V9k zPAsaF1Zqr2-qKnf0AqN_p*dhr`_6}4vDYVoj2|PjlB$EAGN&YkcBD0{MWwjhD6*~; zD>^J*CKl(W(lZXa@oDA1oa|<4+s#Ku|rCy)ZDE0Kjo2QHkl&84*`RyG*59T#d7JI!`)p)R} z2*3OfjlVhH8Z7tf>WZL+MTQ4wwt-_xx!C!ydErIRoBXX{eqCJEZeokI?<)9IF_8&^ z|N0g|aUg~V7Z9E^-mea+@8Y5~i=y2V%#17{hIjQ00cUXwe`4hxgR69xb23)u&kHAW z6xkACg%f8xNy*J=KL-r~ND-dPYLvGv$wstc>Nr9kSL8|BZZbUQ&&M}P=KnlKW-#2$ zs&2wEf$eRXsK|`wMe3>6Fo*B!$9^8(Qn@^nsNqsFo?WYVx*~3e(JzX!+=_K#U@bKa zO%6ZpZ*1p-_?11?K;_1x|z)mEsGJKuQP z0+ffiQFrG&{`eN2LD?|nn(l5EfmMhaIF0?u@m~eOH$%iF6gu+b4i~@soBn?mz=PL~ z+@-!QyU|W5))s#erA3gKS=zkVbWTV3j>;b~L+U#oal`7S2?J(Jyg=K|#KvTu)P@bRH@Us$W!@5Ryq#A&*tG&K5Y@g_TX;yhC; z>KDSOY*Na4`(Hp^IyU4@t_VbU5#W&mj3BbHJex?49`B4Sjo*O&LO7G~h0;-SKcMc+ zt>E@A`NspCY!3b{lLQ**ntQKfiEhfr$7L1)+aTz?fAc=dqZ!Tr*IroQ{al^D`3t-l z9Qp@HYb&eL@??|gVO<7mtk zO)s%8V)^%ri||e=-r}<4@qJ)Zzhe&Km}GF_Zpb(K{75u@MZ#e&-O$T)O>Vw5@j4Wa zMfvxL!iV`C{8OU|KZ4o~s-ur`L}1g_c5#i@*2w58&Xe(*Tz#)pzgD(HMjoh<@4+hrt~v$L}Xa|m;mEVyjcTDgLF<@awlGFN@)t+8WBvmz%@ReYo><#VP0R#O`R zukD?EF--l{-zf%fZX$cdspq^+(Hwg|D^W-Lth9Ew-n@E&+s zT=y)y+*{rDP2Ve9CGt`J z+kZUY#`v>gSWF}V$WY4u9c##DnR2=F?hw>7vn-b|9yg zV=CRdtNwQSj@5p4&SM`pn@IsT&h@RnU|Y>Ed@jit_S|}4cOM@|h<_DC4ttGGv@!Y| zM}|`gysL_v4?UE0hT1%X;IOk0$GF!TT@U2U%mI3r#Rh-EAVx-k)4@SSrP4zYWO*NN zn&8nBZ+S`AaSu+m7cWs-N?dP0&-pVZYY^v;efZprZtxw^M;!+bQCE=$;emg5OG3@* zxXR7}_c61(vvUVCVn$0>Br_+XZq8w?PsbzhA*{J-D*HwcjFi|kELRYoZj{%vP$&am zuopfOG@TN4Z#0?QQ5`3hmBh_}5HqX4DM@2fLUlB2a>fjx@P)T+O7dV#+Y37zI@B#E{KATk zhBb!CQwvyW%|}V_lg*pHDb*MF%~@*5nwlY(ZaD0!WV(noZC?*AnC&$nJS{inJ@so- z;JjOCg|ZemJ^t49@_CW)frXUn_0og)kp^9&T-5yO2(|Dh1QJ=&=SVX_6)rBiA{wQ+ zn-CcL>7~|$bCITb*(9wab7Z)^`$*nyMkpCJ+e!~*#J#H%@wA^4XMuxq&;8a|h=Bo- zRPHeF4}%j)l;3v2&Tb7#GmAU{h8KcEU$38;|05@Bzq^6XtshBO#0N1ku!7(tBH1gmw@08+I~WR`tw&s zMTs(ELa@Q0sxE6HB|Xxwwb5_CcB*9M>Le*+KHgDMZ>TtMTR(IOO-x@S{jEDUo%t#U+HkzyBHQK|OXgaghh%&mN9Kk@RHk zAxtflE@!EYxAugoGUMvlrKC0g_^96nV~8pL;cyyyJF@vMU)_HQeYoh$!Ho8i1>^qC zglAkD3xt^eYoIeY1Q*a93K3XLcfMxu&mBORNt(W??v}rX!@m9BJDJCgkK;J?+Gs;Y z!?uPjZxD!DUOgQo5MqG)`ct>GVm|%j9WSlNG8UB8s#q0)XK#B9_{n*Kj>o+$h-v*L z!g{^m9j!iiA1sjg(i8ru#+3fe7k^pSmC98~ou4dA?p4*1h_{m6bRe;0`*?CiW*^J_C z7WLZWpdV&!ac9Krggkc&wBc<=OEp${Q=~JM;)== zkVz|m02f}%Tt4MhEs?Jf3xD0|9KWO6no2%PuRbNDBH3sx{Ok=LU%!JWJ2o!C3R3nx zXIQ86&o*ja7^=F}Wy=A+vj9`PH(eRYI;+i;@H&?S&|a>G&ti!f#aTuaf7?`TgjSi?3I~H z;`(^6o-77}koD*JD5Z}6=gz;xGt@n-=ty%1wk~-SF0#VD(=tXt%UW38tP&lIAK=vG znbIk*F|Tnjm(QmCExUjWsX#nl=3iv{##_(VIk4bvTd6@$LXOAIpR-oK=tH{es-G`U znLW>?S*fDRh4k7FwC%+W=wjbp#i)Ug-^Tzxa3;^?Dg2F0?ruDk4hlBjQL)U$?60!G zy0dX`dTn9~9Ys7#Q2%aS{3RhHo}`yC$+i9|h^XEiL<=t@gx3wpv7CKt(boQYGpj%q*30 z3qQa^G4l$v4W2&|whz?u?-_DfNyZv}PY*a`c#(9rXzD`oM8>1`SEDOuI^;1Dv|ceU zm%asu^iYSbZT;F(y|JfV46RE2bG)Wqpi1~X_MRi~h`L=Iz3P~y{((2?+hbnTJG$&` zFa`$L>~$qOUjJLRnc7B7fr#vNTlVaO&n-mO5SDaVBI)B_av}6Ok@A>{S=!HD*7C8~ z{~GH`rjrLH&P@-ObuuxltO^mSY_VSuG7P7Brp}AG7FDeTBcBo*ycj{oh7Mm&w-yD3 zk?Gs!Eim$h(Qtj_)ughb%*yqlJhqWpX=m25+f%v+z#)w{+2SJa`|;N_pYn#8Ywn{_ z-hxE=kBC{GlP3lk+wKGLge$;sAzZp%!BdBjzU*SJ_L8QY#>6MJAWRvP3xJL#(HB;s z!?U;F=CBWneaFP%_;X0NEnc4DnQ}eL%;g`un?+WbKSog-h?xVRm$ z$V}*t-YsB7G}1l639{hV(-W=XqlkQ?5WfZXVKcyeFd0*+i zSrIGuWzTUhGQ4j8e~Ajy`oNVU^UQRSiEI#LQc!h?!E{RXC0>n*x+T%^{jVfAxY1M!GGvGS6U$;gK%@8`hwx1EK=*R@BPJ4p`p;~JDu8pHe z2Tf3Bv~u*z?c+{K1~{$(I0o;#*QFN*%%*|#M2 zs-gnM4FezSiwpSfk`3g)}%Gl!Z^79AL^mJL_FfXk6`@JV@{fBxkd)TyEVfsqd>3!va6zJAFg*d;PL+ zhLV!fZkHX6z%@=}bAIGfcJ1t{yi2cas)CY7H#)LFc(g(UVxFC?yz$!*%b?}zYddyK z5VnPlwy-P+U+DYz%NKuFHa)5QQfny*R9`=+yR&ugdRnDevEKW8!Z?D#t5+e&RR4zJ=4ryc8afx=(J);*A% zD!lfSTGwr}m1 zTF?F}*G`fI9EJEVKFm7Zn%7QFnfX@O&?3ZgNA>4k2i^5zTJf{UIfy`iK70Oa-1Em^ z6+?oQjtSoz?dZ_((3zZ1`!{eeK<}A=c8Y zgm0F)L90A8ZX$c|1q4DPL+~dv8=Fh|=9HBv1*>S{_Uo|z2pW?Ch#*kMd!u!wL*?d$ z8N1gI0?Ny3id^zM0cL*U6pe^?b32hu!nA@#?|dSN3LY`@yRd70iM2+&<*@nloq|jt zlFk}rT}ypw`#!R!(O0R|!59MIQ<(s^1#rC}9pl$=SK>pQ+deBjI4X6$yygm?F4y|= zB70e)D4LwBq>y;4Zr6fN?RD)-yv`ut9}FBQRnpCd)QG^gJ?CIE#!juTnQMl!&9blz zDuluAr$?y~4Q@)D6U$n24Z!vjvs%KaPfTW{lip@Cwpn?kFr{*C65g9a=BetO-k8MK zX|+%n89Cqa=+F24r&bve4V0fYTG!tyZTCK@L*D*NTW~Lqc(nW?^*fj9nt2+6jjR*` z@}I%v%}h$cJ8w^~Gj<2Twa7Kk8H|()YnBq_gc`F>{e+(+_I9?xU)b1HKrB-K7$E}S zr;G&eEtlhCiS-3lENMTM&vRY$S5K0cSSk2;ps6)n8+_;Lz#SHFYp}CE1TzITE8K{H zWhyGM{{7{ES)IibKB(Ns{2k*hv1b0h-g{xy(2SzzsfNrN4jC_~CG^d!#Grv^Thqu3 za|s2a4V)1$(WukIf-xxmtcah}9KffPO!}<$b$0j16O&wwjEq2hZ)zuj0hw;aWadn{ zgCQ&4e6VVdE#p~6ujLD7Gb$Dl?&t=)Mwg`n9*GBuEQ(EAA7|(MGfknDyE?4u1>>NS zLpwX}uA-hE+ntbAhC~_9pI`zgp|}7eLq050M#=^!3|vOo@Hx`F`Eo>jv)CG#jyB!g zX7|T4D&<|bVECmzyAPc9tv6HtxvH`k9Z}1FxjtV8FFWK)wAZZID3m4URN@U2c)>JH zDgpRtc!_9^Lhe>@A2Pj98IQ^qAHAj7vW!ugqg8TOet}|+M(-WKci>|6{uc`{JK0c1 zW`WVl@fL*(i>0wFndJ%5n~X+RG=CdUTYO@~vITrlRL3kEzXt+woICGJdD2ffx6_?_ ze|4^Jo^e{kxVks+^g8e))0bpVtJl~OQ_>+zlkjB<{MGW#33K7uU_Lu&bAq};4a60h);2nE z;jbEJ3AEWAn}xgAw|iR>Q?xP*$YzZ9*lp_6h(DuWO!N&e?X-%rWi}I9pmbG0So&;k z6fOzkiK4X6I>M&E)hOb`RCJX+ISXxV#qMZyn9okB$e4VcxY+7eZ2Ny`IHZ8MDJa6GJ5Y_B4PAiMh`)ZAWZZiqBDpx`XG88HHiA{|NDOLb$jTsoZO#Qafs@3MQ4mepfBIU&fbz+y|UI+T^yk!a7{0iT%SbEH4GbJ z&IZbpG_?w2lnd%XzyY1Vy|>2)yhglH%=J^ck`d09a0$xIJjGO)@)OX!#;E$egpZxyJoaph!-lm>myJy=L zPSD{7_9~B2*}-~vam`fgO2BdLctmxot)ca_!m)O&!URf#q@UD6d-8HPKt13ewg-W(`>raq;3KKwmEWboJ-~fXeNP9s6rc24vi8^@Du#Lnjn0?f zvw4WWAL&VE2hGVQ88e}cyHA-hSL#C(cbRRf0e(sB$E%lF2r5b^E4e3u-&gYsJM#rgAB^DZt1m802iGo4nd`28LK^KegXdoR~;KbmZuj0nZnhvUy_7$W2tFV zQ^w{}r=wP{wRkpm3zi>UaR6hNrB|T8Maaof;nkJ>l9Y_Ilqz;cFk!h<^z!l<#4d6H zaTV7K(y1MDAL@1jF3FV3(#GA4AB7KJg!#pqdW1gut`{Vf;t?pKoNYntCwiolVrX#= zP;ywjwp>Cx9uBAogsBPA{u&-!;ki1X&H5_jO-7$*s>t!-B0I1Z!~7U1cWRt>{pP#! zxV1Vi9Vo|=JjbY^q&X6A=Efry`t=1pniQ{`w96nO7tlVUV`EC8&>VzNtCMTamK9l1 z`uKOpR*c~Ta7aepSHl;r_TadeSt`-Pn5;O7*R;AWS$JmgWWPlQlyVCzCNyuH?me|A zdTQCYR+CfV=Ck}l_&tbu9-9uPqv3Kx0Rx~Nmva?$xhS{J(ZzR@!pnV*Z& z5nE!rqmj1eG^he{adzzD{umR{?gHy?Q-O0XS$8OyvS^8HyxUer@oknFlC0!&xf(r{Yd)+U9)cH=&VwP0TIzxuQ<|!EfEz zp!Fhw6Hr2*u^uZVxvb?qMoq%JEb&o=9u&+OED`jo>u`8|8j5Z-<$~{uh>Whu7Gbjg-%VzZJ z$R0Muk9r&?u11OSN+fv~5;v*3IedY|!s=;kGY^Pg>z7h+so)KcsDr!Te=#^_gEKa4 zPkX=6_!t45$bNv#dqe`#Z%a?rr9MFyaTvyqxhX^-rW^xfkEgb5QICTq{I!08UYC?% zUMBAjIZ2f{x7KHiWTT8g3S<|1AHGSMtJ~YFJ8~x=p|x2zUW zCBObrJ8wprLYb201k076RFWhA_BB&e7Y}IvcmU<#1CjQk$na7;@G0(V$g*;;?vci~ z{gi)g1ws=)caw*aY;|-r?0D^T^f!(eZAtf{g3@N{r$?dPFRO>3HcpE? zT5yU#laH11go?RCTN& zJaHVf{tp}THqE1ZY(_gP(TY`~_MDTDimAc$$rR(s%334el3t7g6MYYDSy2SObMgy% zF79N7czu=SwK5OiY|3~l!5E_#`sjpW5ki$^=1N!huHT>{JN)6vN8au`*sBUQW1ag} z9Pu;e&PG>x$SK*_;)NJi^ZN6p@VlclE-3V#VU=6(zC3?Ly;r-~h0X(K?tFyyH(?Bh zTj{FVyWe%8_rK3idXZ0jtqbou-21-}YqH-Lz%p!{G$Y!tkB2O;=B5|JuJ&gZ_-W&2 z{x)9$?xX9S%9=z*wVR`)@ax6k1DTKuk3n*E#_8}@`w+%PPsYZhB-T`ql6s(k&?7_t zWk(f6#95e9ki+d*5pwAS?c{hCl%RR_C0r5J)y6`=vc&Uc6_rOX0u$g^k9abkQ8mg>6I*a60%68qdD*iuJ~{VVJe=RKwq1xoD%hJ z5+)o2(}q!eqn7GlC=a0pMW}zs`(})aUtca_SFtargMy?M^36Yu$SP2x^XX|9c(}M# zbv{OmKvFYd+0-ck0PIiryfgY=R*mY2j*ig!DwwXN(U{_&O|8|2j(BtPh{G33N*YMI z3FyK5(pG1}m0SWtyQ4@Lp(2_|B2DV-j9lS0t8ZJ=T=sO7BGDB=^g{)@zv<%*r@L-| zmN+5eI?w!=g-Qp^QwliS{LS0^511pCE>_;i&V_xR%ziJy!_$o9y-EgusQ8r~7#?m3 z+1Tf!sQJ)v5bXcV6g}pfAH?7z_Mvi?s}Sf7>AGQ02?5@lrKis1LH^S=r4EhH>d93e z%+JqzPtN_VKO1ukT`Et{QS_^OU*Fo8igIbi;BK|pKue84%s)s$tfW8xczB2Jvxz%& zA548Db-Bdc^d+>x4t}z|GW5^lyNheV#}R1?yt1dEvT>)`9=ec_X+J65b4wXz10(6Y z%Gie&%I=-~eFVP^(OKoM6Oj`)mr_)9ly$dxgf=y=Z24-)t147D+BhHN891A}6u$kC zOFH1t8S3ZPj;PO^t3V(C1xNj}rY7Xj!;=-9Gkk_)g|ykwFe+c04w1Frphyim-2(b~ zJMOMJmPCeE`;x?Shr6Q5@iqxtY7zKGp_0!Hs^0du&Tj2eBr2t3U<#k>^X8uK4m*1dqu4z`)@2c{wuY{~)v+U_0rx&x-!1zA@rXgl>;bvzi zRasvgYi4kdYTOS@|1Ha+y&Q26@^5JO@8$6^BKGvE}`FGQzhDfl*Q^Qs1fs4Zvb`VUDgFst7A$b_bvZ@A@K7Jqe3l$t3 z3>weDn128oiC;l#F_txWh^%i>Hqen6*u7MkEmRL}Q-TTg`4lUQRnG6L ztNnugpBG?isy7|>3NP*KjEummvDx5D@H?lY@h^3`?+Ekierty}VBChk9y9_j<<}4> zG73&kMpg`DTjk{BYD8U@G;*e_@4ZWl_;oyt34ROWn@)}S&Yp?M(sja&x=x7rAGD|I zN_H6IT(1q3%Pz;^eWB%-imV(YRK0Ie7pxI@SbP#ud{tLnpGlQECDLAC>ne1A0WYWJ zhcQ8ckf4!`vesb0%#koF@s^uCGt5*1ys`GmljsW-rGy|1dZG^T7Nz8$YjvI%G4K^E z#AAD6x2vJF7HT=bw5Pqwv8GN6;z7cPH>+B`sgQ`s-Yi_R%N%c6Nm++^03%a0RlpRw zo$zOI+BC+R6Ghcl>$+K*t|c4zb?4kND1*Cxcp<+9i%L+f!OFU)A_j{Hi4EFbNZCWp z`MgHr{w;8%PF+lcXC#MJ%!jF%E<5elMC8q zfd;xtxj?@tljT8AMK%|d{_3Pgvq)d}ig(;@M2_BU-LP1=jwCG08^r~?CdX_!gfC&7 z$B4y{7QU_fUu|0Fn&zP1JsY4qba1$@mzS5%=7*s6>4NJl%j@#HV|RYf+bm;T-w(*p zZk=N4fjBe=HJIvo0e(Wq1y`^CN87;Nd>FN>*`DeKMJFTFUk8YIdv(Ttb~JuKe^o?Pcs(HC``uG6;Y#}a*2p*7oTHlB;%t#!3Odp1zdyaEvaoXDSGS9x zT&PGU@dV#DS@EDHrW@$~-T^Rr7H!-P7jcR-?wz++LIomu9^c=COje5V2^CVeRkP`S45j3lE-@mI z;HvQ4ufl}+p!EM4@XiIh6RdRJ7PC?8u$c#&#|X*r*5an(Xdi00v3_H>{#P0KT4s$k z9OAS-k}q}e<78ysy@M&e6YW$#>mqx5u?o~ZVcb^n18a>@P~^H-voT-Ts(9Gyu=rBQ z#o?d-wrNSSm)sm3m+g0Z``7>f^6)uEoZ&p6K9ljDU;JKk`@uVBy$Q_PS3PZ{>!bu( z^Vx3)PHc!`4^HG&4wxzaktT5dV-{a8%c8k5T@HPLWT&Ot`0SXKb9Csizx%@n$4|ICF^KSM!P45@<+Q5j=RvYQ~xb=zYi{`IS-1Nxyd^@I;7+ODspY_bB$Z7DHFZ64) z-hsCxF=5K=)FC?p4eUJ==|X`IN%v9HOAaN5vJnH!tnD@&RA4hdoS%VfA@37^Crhye`QU#R(UcU87x?hV*n*^7eYx+7=5eGV0${lX9VD(sq1(~a%E z8^KA*bIM{675&$*#er>Y?^cas0TR_=G->k%-|AG9cHw%JYw@#cqs1^{hL6cLFy9=2$&^&HMO} zvtJLc*1cj7sb1<*o-eTux{SPb3$^LG*?OwIsCuKnR3$w21-(!+m`ZAZ{de89LPZYN zFBJ&ZMMmLqZGNjKrQB+||KcCqv9y6}h;dXqt%S5hP*IT>k-5b_{G2gU=-<42xhnvr3K->R|HKkAA z9%yPV-T=MR=ZiAezC0!z)4+{IBkd_5zN3t}CG<-&-!mAvZmd<84Q|Y;B+$dVzt2;V z#C;!X6LD@GFSMwR7b=NzZlQdz+@TFd45}8VgJ9H*O`e+(`+o?7RKXGbM0UORyaJ31 zB7RZypb*sCR)$3K2)wBD?EVjb@V)SKSU$c?jmcLoy8i`B=4STEVz~LK&ZHOI2wz{% zMob*Qp`BR4FKXZ!n?_m#_hYzNC_$7m=z^MZE=Gs3|&l~6A z-T##9#+kRgoQIK7^YaxICLK9WBPY@EdXXpRO+W+|M-G>fKvW@_L$rsHT{6NXHR&2l z6;A1Y73+oX$q@&~{t5KUgtlA)rCB;RDsjcM^{v5!t zgW`G?FmktR_OE?B!&D$uA`+V(lmS8i_`L3ck9&D*giZh{qz5%B8%nl(d_0b_()8dQ zjPcgA!P}yW`>veGp*ih~m~$nGp)i?m=w1}jP<_m?+b{be16y7`devSynhAQJbp7#p z$U9hpT&p&kT<%)C-YhSlfPuD9Ah7T-c?Jv;XhfJI5Y_yKW4kM{0x2Id1UD+=UU-& zeN3)~&kD!cV{*lW>RoPGrN}^xe@j^eZC4|152NMu(S^6CUCI>HW83SBfa1fE`d_s~ zyzz#M7#>J2H#Hz7Nbi}RQ4YgBTg`v32ho_>z^1y@&-9_9$3GrElpj-lJ>?Jj#foqo zpR`6X_=o>R8v&_4t-|gTmHKJAk;q4P09x9Vvszvd`#7u&+NS?mHKnI7HC z@+4;J=0M*XBO@d8Mi~ocBVNNvpus^yHKCOe5UqL=jP(tO3`EKOI3vI0!xay_*jFCi ze%^t6sZX-Dg#v#**oyAPYgnk5J4$yg$AE-V1#sz(R7vF*nzsa3%p;zDI#m7)-KJZs zE8_tNbn7wS1+9&PiVRxdsP* z{**e6*nuK_&*vB^9glGO9_bVhH&Fip`d;HrdGV{1vxSh|E*<9Q@2aTAT*Ts3Q-~Jb za&V~cuYBQ&DDB5x0(w+nK)=9eM5zPMoCs?%+DY_X8II%#R2UrVb3QxkZ6Nbv#&#n; z8-DWjZ|7)av6E}w;(b;kEnbqchTPePrKUHN9^K?!qMi`+D^qO8y4g#iem*1 zzbw85*!c>1J-Y?XQAYVHEo2=t%O?O zFA$N!XW4;%-#>U)S*V5w1z|PI0r>2Q8$xm@n5VQApw5f|JxEUsLazt>qx_6ahYE&f_gd2TuO6D$xj_-{T^XH!%sJ{f#DVxwm*+#0c9-IJe>th4ibZ{}M@lYHis5jy}P1m9Ue}U8ZCFz33$4jo;$M( zBg-6%@$bvK4^}WWH0=R}urG&SZ8jZLBF-=kyDg>}U!0F0gI+ZJl%hM&nqFG^pJnYe zJMohOQk^nzMu+{2z?dHe_HKzuq{SW~#Lmjmb+y(rG zt0%rjd`vs}jUda;rB52%)*~QP0kiO&+*26`p(wmY#vKGf;6MZw;fJDwU1i0_GDrMG zIa~dVPs;8IkA9N43gzFrCHlIY6~!4Y#iV?{@+Q@E&>w3)T25Fv<;OiIx$Gk8LtaU= zvb-pbc6)q=<+_S9B^Ia1!f3TKbN+tENlmyy(&t^3F3Hq;YC+CsI;o&(*<|}gl3~%`47INjI2!7InaVrV^6~?}k;P+g?&TI8(%lGs> zBXP82->JE5>_%%a#r*J(%)@wVVd4DoQ@qy?eQZm1#cB|qejrlc3;Txx(fD4Z@*F+~ zf2yHqlmG*V)x7~5nBpr+oeTRJhyBR02(~bIw+BXslsU}JzICi$e4`Q5=kBA-Vs7jg zJS~!T1u69}>VJ4&~9m!>mtvr_BO9Nk;SXR^8=?2-&p5UP?1H5J&uN#9$N%#886;v6-@T*4o;9C zlYQ0hJ|AfIoEcrMUS1CkaDMTfnxnTTLB*FrcI;uaXO(AF-8Txs6v(#qvof?|HTBYj zevD3@hrG531uJK*|L&O^OyeF7dg!X)dl#3;}l5+7i`IT zwog3Dn|s%1?Q8o}v;6Ts?Q`vCp5G^m7%zM6t2f-}7&Lu<4@+FxcP0%`_g3FCFa-S!OlR~?GqDr$pGhImcGX5rt$u01d6z-#>ihir}@dG1s4G5t-z+hrzT^4||=f^q+hWG+&vE#P^ovml*C&9vO@HbMCLM6{`+ z+fdKjtAwofGr8-H1KWQfUy-mrV;`ZO1Ub3+>8&1u^4Lt`Q~CRk9G0$GI<54UPHov9E7fSQN_)SCR#JY{N zoG(W)53O?GmVDoCn`?gjud$+!>0SPUy!IN)3#Qw_=|0SoSMxPM*4qar$z4D|C;_%+ zfODciE}V=NOX2(dGVWhs%Q_re2h3cu7t4{+#{ADQzt9_(g2H=` zHL4t*orP1+{)*XhtU>W$nE`QLA739GdGjRYIc+fHJJ5S65*!w!+TEi;h^W_hoo_I9 z%*}**(?;kqd+n+me|4qj`D&QMNjdiKU*Lp@R-tT$I&eL#&-@o+;Vff@6$HGXI3_h2 zC~u7zj)>FY*EkrLDYnrk;48;YR9`d~_O@A^`P&K5XD_HS!b2Uyku*-pumd$~YpG1@ z^sJH|iF_sucx6sQpXY@VWP(iGAXcR0@RfXNLg#%w)leP{H|u-8TtsQM`VF2(4Orw2KkBP1(!eZ}4EH0I@*lK0 z_kBld(r%5W5N!R!LaH9I%=AOAQh%?oO% z#ta%Y!KvKklYDgGQsQF7}UH|dvK$HXTc60jER8C=K8%XW%&RB?HxQ< z$J}lO$E1@Hp zw&{(f^M8s)$Gzo&i9XMjm)rhfoBpr5-94yQy7T0HeCw?>2?&%xx|ep4Z)TOzlGG7# zl_zD6FG>0ZXUwv6X@iGMo#z|CRD1^xnM+q~OT4AhAr}HOOJb@Le?wW|Ld>FX9J+mT z$|`ywcC|Akfr7wQQJOeIz+Y0TZN@Llz3s2zlD}o}IWLbV57P`#2KV@~rsKQ=K07c# zG>!A=0TN~Rwp;l5 zTUs{wG=%RCOA1TVi*wfY_L^aPqjcb38nh`$q%b8AD5Fkk7)F?V8e!Y9S7+7>S~k}E zMnQrkt!KAow!4rCI33FnH2brA=kg`U5y$5(<5nwdZ6UUQeZ3j(>TC}^x9qTV)RcYIV(tiL)Cr-N4`>_Dh=(@=Ou(Ro{0>4$yF;S+3Qo_F0&4 zZxh@@Ue#ZZvD{D|X*Zr;|MMnxdjT^>jfp&~Y}hZpUu^m-h@Hzu1uRjIUG&$X&xF$l zCim(ZMAKGn^me0TTi+}((%7M9T&h9yZr)WH1DDlu#{3B$9!;Cr-Rh>*v2v|!D#|C6 zgt?N`1_nefc=!?dICK%oguUJ6g|bhI|AiFjg8gh`iTqkQLFxGC$2)#LUVLE8HZ7!b zD1*@5kQW?udz}uaiwpe^tr~?ysAiro-3dnQH?SAm7Zz;fX%*%Z@cqF`G(v$Xf1$OS zWO);1uGpIg*EeZ zfk(3zUp;SgfdaL(J5VE|TwVRv9hW&Y@m3ifWrM;LYw7oJA65Vhn+ms=fY0`^gqx}7 zGkx%Fgl?Ri==saZ%8DUxx`0&PpyH)rZ!^b~5+xjbqI`jF>gl_3gs z>B?3->yqN#-elYf6s9Qq9#Xxj;>T(E@q>eF+$jn;83k?5lp$-VRAVb0B0=@5$QvTb zZKo>07>V=EvJaYDNj$I#7KCi=e@)a7j2>nlpBBX-d_Qvbs3Xp@x*+U7Hi=& zlNLbzFj*a_7U91NeZxXFQn$gUC@B+kx* zEtxPX9rXjeOxkOP6(ube>MT~#YH6Gq|iIRwD@W}l!9w2}d`DV_~+ z#PqJH0%>o|$BE+n9yZt3Uns3Y89f7nNQa5P-3uASf75jh)&jfOtdtpUyk!fuir;@) z=_iBo@VlnvrjNT!`7%PW^$|cHDroKvANBI`xVuSCHTRs@@%Gg2tyfJ>*mhiDji$TI zMc#XW9>xR+qtyO)Yeyy7RWYoNGZF$P4X1kJ9Fzp0Rjlh^eNOUEpDD32;*_^iS8F9n zEbVvx9Y+A(_|}`e{rsprJT7D1rWZv=XAh~faLp-m)SKENhCz~liSiPDRa{ib{Kvze75u>e90QA7wCVqdv_CgcO4#f(P5DnN=~+S(C_i!iCt|@ zyj4t6N3^B@9+7qt!3HQo`n{k`a)jECzf)dGE(QwDDq5+ z(aS0-xQMWUL7i!(lW(OR0&#xO;ZnaKC1IURXS?D0EVQ;QG88?oT+#!KPK9!zeqP@w zQuc#ZvEXJ)|6pWjx~zbG(})+R5EP4O8IxRS{`qpPoA#C~B65Jq4Wf>>MapUI&$|DN zu4n)Fn988d^^YRcnlT@_*GN_tudv$wkMwc(>YadH51pWQq7QOS^cwzj39F%80G}th zA{npc>H`@{^sDDBV4#8E5f?0KM6ZDT4L?mcq9;37y;ber`nx_1IIS2b^U+ zEK3d!I(ikRbJ0%^d{LKf&l>-ObqzyL2Dl2<4^{Ta%b%qkq*pB~l9`1p17=XbiNON+ zIsVp!n?r7abhN&~t5iu5M$UxUj?q8=dxN>Y6LV0?wYxkysIbm{|Bg|7^OuB&qrZ4$ zQb95Goic@;wf}Tc2SL)L&XZcHzfKYFn5wbp`0Q8oy#{Kk57ZVCoJYJdI+s8Y7x@X7 znMi|SVCa#f35fE^iwFF@i+f{)|K|nh?bd!lT^b{9y8FXCdsg&q*x{w2KfIfz>}+LB zf$cC5N;XmfKPpMLT4rtv_ixGeS`zSFvq87tzfriI|elQInR(1dOK8URldN_7v#zxRD0{> zl&uva&%mhrp@qtXG1e)$;;-H59Fy3CnDVkB1t|-BU4e=2kn@y;N7EQgSK^Z(#dIEs z#QQ^IMiH+~Jb%JT`>cGU{KtGg-sEc~ukQkMLe48XV?R{0(1(rllU%i=_*Jus>&Dxe&2b^}*2;$?~8EzDg)hlWP z#{BtmAW>Cgxw}dPPVhHBpmIOWzg1x;@$&zQLxWVJh_)3YVc7PO*1Hn0Bq{jKjSx21 zV0m~_-YHwpRpdJFCYGz6o!?(>J}o*7(03>kNrk*u>>ytaa-mb_^C@7NFIdRxJh%C} zHmz6<1oGIpJPO8g{M)SFaKl+V7!(;rdUC3EI@)9N`=(K+Mq2V`cL+<(p1>w1|3$`M1zusw;~HNu8d;^pZv8^k~L2u+EI(imG=IEDC29w-~S3i z@vK9ntsKF#L|`G<72%1th&-4k0Epw)d#rqmjbWzJ5L_;ynP!+dI?yq&65=GfD-UZn z%dEpx?Zr%KD?VB?6VE#79-|w0bX4x>m$+AFOYVi6hc!sHemL{m);$#*hL)HomZA*0l zQbYA_x+}V>BR2T;#WM1}RO26MsC%^kK^${RCEoSF5^(kReN>i0gM_S)!5sWwG8!p+ z`3Dffg&w@-!M8u@?0F_DEZK{t>(P^T>Fw@o(Pw-k6!k5MQGki*-YQ66)GB^kxcrmg z9+80*FWl6NLx3b(g2R4w#(Ws_gpU{KUK65e5}?+px(^W)oJ$8A%-`u*o{i79j3{)0`-eKEHN=k;%6$l%Ivt3GhcC#V1ppg8t$^DgJ7=EW9C1oG_ za)m(dBCOYFDP&)??P*+Ud*C5jd@=N|W$1|!`Dyyw3-JS{NU7Y6%^l3x)OcD2<@rGcH*-6 zeBpQ>IQgZgwBpdwIYoDIX^F1rP0gpAY{C&z;bTTCy6pgR%K=^{4oHr;XDR&j8pI|#f`fZ^1mx|Lz37hfuZ^rIeO zwEMfL80V3f>11c;oqlLnIMFYOqhk+8L)y5Ha98m+K!Nxs_}}s}4E8;z)o34d&$(u5 z?mW~;3~(6w^`cVtwsb*^si~huV7^V2oz6UFgmwf3(a<^fhR`Z*ez3n~s}x+m>B?v6 z{b+ic(iE%No0rrqB|^FrA{5r20YdjHbgS=!3Kr1(K5gV49#!7sJX|;kLPM)xj9IM(%f1p3ffs z92kgct<0>{FbV>)PV_PTot^FQo4=NK02V`!v-riF$pmhMFy-*38VCraMr_h$d63RF z;*7yQ4|w!FQhbVK9_L75WB8x}arlx~RLv_f1dt}+GJ(gRcU$_jWN|zI>WM*uPxv0* z8C@{MCnQAr?VI8Xy14rhLqkLBUV#BK*|Q}|NrsgMu;*3NMiQIpb)=eO5<-NVo13l6 zED=#eMW*3oDRzVUn((g`_PSK}?oWjNyn87ViNS~pD7H@t@IciU;>QghQJ2iS4Wq0x z$%=Mctau9_v*en5`Jz-Zyy;D8QqzcyT$uhYXal_fJyoY!7U{=)uu%OBJOhBTQC1mN zIz*>aC2&3gcsWBAc1=E_hnnnw2)&cN5bG|=0rJQ4W&c|vYrX319J_}HVPU5N!a|4t zC_sS5V7sAN3CmlM5JYZ{7WPR1{{Mrp^HypTsi!(+kAw8p1Wo{<$pVmFT3T#20MP^Y z;&edR|DZ)<`p-yy@P9P}dcsoD(t{G==dDXm&dMmEmjha1sfADkkP+8e_ z?~o8>Nc|b2K0Vc2RziZUxU1{tzW`$fB8vKzSt}5G{F|ULU9kg@F5!@l1kO;vLOSyA z@K77V&p!%t5PP$?zpwZ)^9@_^%}ppMI5^JX0rT+YpXq#U*^-M7f_-gLZ#3H^i5dHA zgjY*`7~A1;#Rq1;E_?VvX}(~KyxiU%74ofv+-lyn52yNOgE*ppE@6^b*dp+60jx3Z6Op`GQ!d&`w>t|hr z^H1_frO;qP!kag-boN;&b&=1pABZ5(Q^4_Ev6Izty`Dba*m1Kz`{Q4l+`qJK)fkZ! z%Q%th_*f^~A<~#0rlKr7hvV8D*^O~&klHlGo>wi*=NtUvof7x5@9u+-73rjdRtcS? zN>>E(LXidtGFnFOb^&j(Y>8$n9b+#Obbu5{To2iAu39eES*s4sw5R|1r!y6Pc6O$# z>LJQonr^!LA9}Qge_EEoWEl8zNlG;1v6c>*R?6}#B!U19bPj%-pkSj9t^Nz;<1m01pR2^0`(m#o|+Bs z0yU1RRCnczE67rx$~y#Ui=Gld7}T&Q(98DF%?U>Mw%xKg4=? z`m%{lGP+hbK+=&EV=h2tOJ$|+*GSYt#$s?*`;Y_+3rl(Wqj6tMTDexkd}4C;Y#%}j zWblenRnbmtNN2Cd`c%WO+4M=u$%!|Ji;D|6%Qsaq6vqqlq^&*F_(^n9H2tJtH3X;0 z09?mb?~J8Q&vm$djqCQEW7`Y)?&|2M7B(pvar&;iayF>VuSU)!KV|06BKS%8_J|DF zX@dmBBYN}>ak;nc$g{3ydHL7Wc_~^=x`*3pW4xlZ&#v9H3dg144Vd_weY?0nK#cu_ z0HC{Hj~7Yp(#RVCh|U`jurDf2A3|=<16*1kC?T6+V2HDK_YcMId`&B>m;-oq;4j|19DN+iPNFM@bAcltDql$&?D& zx+4$`^-nt65=($rd%k`5DlAOY`{j{m0>v0ExK8)Mi)MeipF7tJnA~n!tPjLgRFk7r zeNTts_Tp$2z(!76hTrTrF5QkT-2!k71dX3faNHAN=W=L&^RqVD&`Z+fS(h@zyldmE z&*;dg*T_kkE3NdK@AKH>(c8VGjp~0jz3YL&?v+*8K8VG8I0N!##liPs@3x5m-QPNtD!dw*&LR)AI-8q_$ZpJ#=bT zgF>1{2}a`qTb%xG%y_olI({UMf&V~!{NJWRZ?em<^V5I-06$R&5FW`pNeVj!SSnKs z1%(A6SDV675X3YPFa-TPC^5@QcmhU-;vW28pYq>dT>DqR?0t88Rhm93ceen4-*FUj zKHo*hd55weEm+T!yWA@I@0=~Ho_Pu(UFv4}|$$RpIY`a$asv}?vvE3=kkB{8qm34>kgXK=+3G)$= zFWA9yDpo_EEA`$zwc)hU1HV(&FJ%>kQ}!>Hr_?@;brm6+agA0q!%^qJZjgQB3H-SF z`0-bJ{7;l;V8)DQ1|mKz@~MF!sUG@~@VkW5If+gbZtX8)X*Gp9Edtc;djL%ZLaTZ) z-wx&lv!0bL9KYGIa6U_}ZZK)UhGn$64CZ z28Bw@*Y{`3&X48o*R^y+`!0)q54ue|dOIz_^wD?DDqRHqG1!kP70v z(-jNORv>^@s5}|oZ)U!r#}-DIp_1U^NBnvuH7YE6$iI%q`hzlWFH#Y7s?LNZ(G9%4 z0Vdkh{5M=sF^6L|ld%VP2QzScmGo)P#JQ{ut&@z~vo~S7g+d^Bp3d=CLyVRX0H zGV)FFxG_UxqF*nYNoB2Kl!t8`)EaajZ?OqG&yiVW+4%5zkt+}P=F7c!&W&I3^h5uC zTfo!hFU;yhy__dbpew^Sa2

    3T|WENT*zdND!tLwBZanYeKE!5V|n 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 z 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�vyw{>;ethMjhbeJoP!@j)h8z83rPbo+Y6dZNI_humJ@ zIs^(EsPW}47Dz)Gx296juuB&6mX zC%ii@3Cy-$$t^c#ibiy|4e38tT@W$KuJ>fpMYo{GN~2(t^WL33+>gf6UPr3CRL=|u z9Eh9#Kp*)r^v4xxvg>=o@tLKY2X2Yn58dOh*ZyH(4cn2iuEUt;M9Oce;F={c*l?v2 z1dhUbYo;&&+!m=u093pwI%#dopH7sy4m%TjhXVgUuHG{o&amy;9yMB+(FwsQLG)fj z7$sU5MvD?9qlM@#Y6zo*C}BpA7Df;RVe}fwAfiR|9$kpu-s`@<=h?n@dw==i2OHO% z*SXfQjqSo8#1iQ$04(jKpAv3KIw1Dc1>atJ?Iy7G zJCEV~S!QDWQ;xcdsY}1lVtYP`gVjAIIuAVszlhvpN0U-AZKSH*>XA#bo`e+Fzn@Uh zkHSi*&wf#*fa&O@_IW=Qi1hd7FM4fx8)d);AFZy~YS>EYw=!dnMXhj`)o)7Cjz~$b z^;a1hTYjB~DkVAO63A>Nq>`XvEtT(4Y1N&-k_q4W(G1cq({Su^)A2HvjIqXzdCf)q~&Wln=o%ZfP>COMVB$j0IzboMrafHLJ@_+Jg z#y=eY&o_dmS;HA25beA)w@MSY$pF|mwN66emJk@I$Pi$GEvlS(`=qW!Ane`dbkOEd)t_^{wINZ< z&%V_g=Uw=E3ds+&=Ku^*STiXoPU314O{NZq zi`7%ofaO(vaOub&9|%iVQ_jU~aozkwp-KW9zF7#Zuj40gF@5uSO8pxz0V8ITqu+*g z_Q9mi=pm(181`85j#7WvXXWFFCK=@13x6nY&LemXudDv+H>?o9x&eF%PDVCBmza}@ zAf$0VEpVR69&p&Aq%i8sKPcYW-fWr?2OaL5xSrAgsmZR(bB=b>{SBvl(CNnG2guIv z>l~1g@m>vBqQhbF=dvz&sHKvn)854!Nud6djRpeAF_mJBEa3QeS6IOgD6QT&S&l&g zGmMGfC#_<(IXj@|GTDF)?fEiv2liKt`eBA$KqXc{+MyOQpy;h3xHyn4L73-barHZgD!-6YZiOIf>>DkJ{Uf=QFnYidkZ7NDXSzZ11OgYPk&cgF-ZPJ^`XEOtk7tHpZFiu-ib+%XDc zkbD7}e{O7ppK!DiIUo^e&l7t^3hESkDi-o+&VqP*aWRsg`h*4_;^U(Z6A-PN&UbPZ zh3f1?{5eq!D9NK7k!k1w?(O!8qdGzah|JMLl}jetkXv zS|Iivys9eizfel5oGuSMZ-gB>XX8{%m6<25AmH$#yF=A17zCAF(D)+ZIG)tp-ah>B zKJ?-ckYSl-W{j3@B=U5VRRBKZU045d6ec%6KYNhm-2^MqeE1De=ko6y=P47x)nAIA z@ug4M@t^*E3XG=!pVyZsKL6^U@!Rl&fqum~<>}l=Ry;eP4!ZG97uMz0YmNwMIuBJ# zh2h@smh#XaN{C5nN z#W($4Kj9Prsnj*?(PS)leKArcjtSTX*xvpK)c+hFT5j!J+zGhp=C}s`N3rf<&VY5q z9Mn+boux8py{M$>wVWPOSPCjg4lN`awjum?y@QJ=iJcQ;_jcj!oSl86z>(xoc{QG3 zR7kj3p{&rfpX^A+-w$miF|Uw+@Hb^>^*Zrc%atpgGVeY{W5Yv0=vln?N%-B zZrVs)9!#CJ$w$v_$WgJ>K}PDFh9x#R3-{*Rl2I4k#1T!Bb-;wuPE0c3YJV|`>;&f? zoC+BMK%y|;lIEB97$Riz&A5=r-G=ElGGp-5QrPbM4+6_BBZ{;svmQ(kcUFSW$NYV6 z)S$c35sF!xS66n~5XgFk3MX4E_UU|aRp`SoxDw0*prg$X|&c?@$7Z#(<}w?F_Nx$jC-Vuw;L zl}AqbS2-wv+2n(4YiSWIDYkNSt0tIvLkFV%`bw9Rj1Wj*15usezkoD5OGpMe&K#`G zDuRG#rmxG9fr+>oK+68XmbylsIWEqS0|I1s;k|s$tT`tk@+Y!n=w2>R*N$rpnA|Jr zsSZ1;_#NPMWEJh<@c`x>H2rVY%(m$Q=*vYny%GGmBPtpV@Hfp65teS+35qHVR<@_#+65e`4h#&@kpdk_1`K{!OzTFzME~Q{21RsF(XsTaBl;gvR9^ome4PV z4`(UDAGyE%q#t%tlT^nO@x|u=cI-!hpP8%=Is#UVU+b29)&|TO61S>LoRDgE zA|7H1i2YeSt?2-ML$0}l3Sw6&JsyWtDqraof+{d(ttOMTW!cMpST@+4%}#0Rph#(i zC`8SgI{w?}xww~%sClcAF?VqR>Z?`G@XQU7r z+IH4@KLLufXTSHsVf!Cs*p2T@jRSvk;c}A4=*z8^;grI`%Sqj#dbeELyo{cfPawVT zpU=%`oa>p3N{)@>BV-3sCstYEo0zcl@|pdsuFlA@pjaPb65v7jP31{Uuua;1X&gY~ z36l%SwQm;kJ&UiOu>9VZ)?t}pqc}+BmJ)@*P=HEl4Yrt{>;;GJeo*YHt$$#!k9v9w zL=KMb=2j!5<)m1f+3cvd?E*_PY-NjaloGs1AYsW|4OJH2Yi;xIC+~?$Fu7MD;wNP{VTMqqi7C>XD-=lz2 zTnkKyxBB^omvFhuozh+`=xtVsKODx)6`u7hubvd+{0wP&D#nJfnB@|(CMg(keYYH_ z&BYGFD(I)7*peM4?x86w_Z_9*ro?vh417dg3UfR;b``LK>b!kRDX#@utlDKwd7v-S zmKe#w=D*`BZhLRT>T^Y0)Lp#y2LtFwx|Cs|2X5}^YGm9x$plQKnPg_>3bLQ20tQBw zAp$K?EAMnBEi3BtO^lx@LMbMxIBLUFJ;9KYbe^r#uEMVZ@<|SDbP3&7U>r^+nd~+N z<~4_p-(c;D)zs=t)zd{}MH<0Hp>$R<9}88NwL!DcbNq!GW3bs;EZ*!jNzo^fHMe_r zButirTxvk59AXJukxBh2F>gVJ2--mLpwAOov5{G?qqyXY z+rgSvTRs*i6MiiubPS7@UxvQ5-gOL2Fa1n6En_Pv&vNtSg2GHfO`w;IT9=~+F2^N@ zuVb)3j4bUMc5gIo=S%vX(2ZvCCS|QZ zkIsZ*h2tKaT|qiuMLe4d3{@~Mto4(Yd$aq$r~TN(n}hV)$2a)k{}GpucWx3I?n)+r z>h`tb!i;lV%cB=k#%3Ci(b89!9*MQWR|f?jmnIiQRKC7$5VlykLJ1Hq~As( z6u9@oj1Q;9iDFvWwV=?XwYFJ$NnEi1!>&@uighCwN?d-_e^ldFX*EpzA<*Ydy}#q{ zFaZ;k43WTwv?&|^I`g=d5S}s&-plN(65%>-(Al_hhLydPsAe`ffYvaV|05l1 ztZCSRL{}qw?58(L(4#3#?Fh@p^!eJG2W6Mw1#`0m~pg13jrc3 z;%GuBrwRW43VDZ(?U(0mNy296!+$_){?hK1hZA!8TBueot;=di!*wH&nIY0essq}g z7i#%Qxh0o8kkil8OM?#`<|B zB_+;NPapRlj=6RpA`(9YnGNMVBC~EZ@+JVu$NBCD{pUW-1E-piWN&v6`VDXo+jtCW z3BS23mYa_1GmZFjkw;=ZDSpx|wi1_%`WYwdmXRH%c<)SUCO3-CDfmYu%2F~QG%V;5 zYp?r3kHc@%#Wx8H}S_Ll4;{=P$Q?ke3IU?Zm(&bZRC# z4y9H&rlvk&9sM&_{abJE>oE`JY;5SwE-NVIB=kls(Yn$av&9Jgj8sKeu%LNir7o@z z1^xbsTJN=8oR?lB0@B8jR3Z@{_-inhhWc4vkP1ErG5lENN(y?ugS>ap{~sFar-9k| zus~poGyGCQDNB|hfehd0^-p6CyQL7pkwxfzwDj8I`T1b6m(l|m-W(|Zc(H|Kjseto zf=iYw#V8R0N@}`Hik5R30$l4SY!ko#ld)k(VVH_e>!#L zTQX@4A+I0m=HKzbeAZx&R_@TV8^U)lYIk)P{O%$cWgdKV`rP(S;oh%VEkDrY2@Qb~ z8!6@VixY=C4Y)c+krKy-ZW91JVqFxmyY~XqR87J*tFkHBrkx?Zb~F|!q0cI^Q~O(FQzXFJaZF# zcEiFNd?xj6XOE7L+w^bm;xG62IiX#K3E!`#3U)3pf8zbK*&`-CZpp;0ga-In>ohgt zL|)IgoQa@+XZPW8*7-lk?KgEQ;k$$IE51Y1lCM%M9pyFZ$l{tQ4_{aH?B8C0+yW%^ zt;+$@^pL2S{+dn$v zr8a9crCjQ=dEq{J0wq~^n5}3fTmZEY&$^1oQ>9Z6lAudn&QF(<`@LoivaQO?#?~04 zPbMcpNi;Mi=_f#s@c!kQR$3BH-*3!`=?>Wo1(}|Hj&_srL#NDLh4SDZy@%@r*VzEo z1l~MU2~cu}`vy9Cg#c=G{oaMl)YjyD#U_(&2*H9Fo5ns6|Dor0vTvE*x{jjtfJ5`r#fWPe0AC6w z?8^LMYQOXY-^_-e&g-OvpT?>L8_vHN{JYq#|8@l(ezb|$xaeN|B#^3SN_{kNj1ykK z8^D0jf`ysn@{WH3PhSk(9kZb=(U}@m1f_2K2D>JVw(i#O^{Qkcl5w|h2><%aw8yud z*%Z_}Bbd7{RP_QzSfE&D2YzKdNZ=z^^{IypUoZZ;^YP31Eo zBOP$FzomefQyl6F=pb*zg37dQ&o{3eEzgr?X=v1Ua4&4bZFSCo6^o1C0rR0J)|KPR zs50%xQs0l?bl?7ucNWmDNbOM;O4DDXKhgn(pPjF@e$6?$_PkxlPxG23bpDnFS8A`? z^6qXDrh2~JX$_FK|3YsX!!J}khK~V^3^cyw!4EqW zstuU+cZ)f`n_SEuBo*4EljQjQHYU*Jh8gnU5xQLK&e8L5f`d|r(ZBQ8(>L;6Kzp!& zubTjT}&g^p;$miKzG^Xrds6O0p-jD2oqM}bSPyj_cfNa$A z+>vP^O9(reeG(9UbA20vC!*((UR!FVBUnzx${Wmny^~tO;fGoo)RU(ER#A(GSM@fhh zwEFbi+_Pd@NGRQ9xE|Jv9cG;Lsxmy1mxih-v`GnH)pMW%`^i_iOHYk=`eRxA%6mc#eozJP|vzysSQV zLr7noJJ8(wCJ~B8@oUr=uPd~_!ps(>Z<3a#I;MA73r2}Pc8p>B2K{~)QYk(9Rkwu2 zaHG^eM-zImQoTZ?28-^3nfpn-pmwW*iLl>_#g%etRr)cLA`LoQr9*RQWCy7Z61;xia2$-nD?K@E9;;Fht^(G6@VhbGV^7YUn001}FK*2wmbxo>yaPPhik2 z49Zz5H)KQI{p^!rG^ShWQCLJb$83yuN94+i&OzI^;Ha&P&xG;Dm0`2(6S(hhVo=dA zV3b&uH%^a4DoIpb5LBDt^o^B{;+C!S-;q0ED zR=2u(csq0qkr_!BUDhA=M?N=Y4=Eg}TC#c)FUEGNgU9MEvS)Gz9v94fz@?&A6b4LY zTBoSeZJ1anpMApBu^fDC2VtgP2b#=#%3r-(pV za_Y6sO57?}Qzba}v#NIFHM2lJpAb;+e~ z&))D$d;wkY3$j^w@WlOi=2vxPpG&oA$P-nw2 z>^xo2wF}7zi>8G=r55ZgACnudH|JkFyMMMPR91ndGnMe#!G$Okxp+JoupekjOSvO_ zZPzBpv}TTweXE$rRLYa0CFhk+QMXpbQnj{|fcgz%8@Tu9d}*bVOAYB>H!u7ld*(LI z>exa2Tn9{=%l2sPB2{S;Uv+m=MxPE`{)`oJcd$r<*ho%msQx4I$LX1kjGDPN?6I}h zz|mj%&AgMA*I2iVVmIU(dN9zQ-2|@(PGNnBg^fW3-)E(I&htt0xZmedY2W>|>H}i;2h$fg*(?cv;zy8X!Fh?UBlLt6R{3Cjw z@dzWDL-Xm=$nH$n)OYX1Zna#)CHnFo#sGM+ewsRBd>~k$UhL={N~BtZ*VEsh(Qa5o1W~VJQRiX} zp=1)+4Qs*O`ZVhSG-^yYU0m$V!=in0C~Xp_c-Oj(xYx?@;~xc#gr+KBMeF4Xp*nb~ z|8%5&{!uC%IvadsP~LdbP};xWO^4FBW&S^!1j)0COd1;YQ{9BpjSFyZdoV!7h5s zKimUp5ul`~=&+`QH(36Y@aiG)%-6a=EzZJ*6{$FWVRrA8$sJ@az5IYD{GnBXh+Ac^ zwT5H+{G=t$03DR22?_`}fruwXVd@!S>4}9oEwSC5vwIwOO${QFh$0RTzr!7~4kU+t z9>jYl4TOl5%pXIa!VTR9@lsM<+0sWF+uKz8N?e(6)FZ*Kos8H)3d)hQ-q$Y`XXZJQ zyH{6Bh($z20b<=T)N^R2S{$Pwk#;wSPa3 zs(C?36=Y`-<1RGe#{BB`aIkb13!L@uZ=Q>-?*SFMNLY@~rKiMUg-6oF@ifIL;qBVy zW)X`N$UW%LnRVFtBLUuZs)$*8h?@DH46N9O8v+{rSoqW(SHPT>+adoj)=%6znfod)EAZYK^chG+D`#A`I*6F_ z=++C?n^GB7sB8T#Q&jCa3bu}y*1rXcDR}BZyBgU#tSEUu?*l&r2taj0GvdYjn6oGv zrmQL~oN>&{$hyB&209y^QMW)p{BD+ad*9q#{+=o|v@iU{$L0=64;B4{um)k?R ziulVd+Z$HCa&@GhfDogwB+*9`KOtx#VMpg;D&}Ix0K(}>$F$dSXplH!4X&EV>=v`C ztd6x4qmwijVU+r7W?*2@$4a9rT(ZJum6)PV2j#|0c{JXy*RBEo+!*<*Z}iCo@vac+ z9U5k&vk}X+t`u_k1|_ET5R_>RoyZnSjpZa@mR<&TfEt4KPq&{?N`WDo%k(2K4GJ;= zk}m7=nDbsUTOzliEB@~;^)65BwcWc06u!7W0htS#p_t{4Lc?2UQRXUCc&^OfG~G{L zC`;8XkJ3pd=o^k(1&_0;sioR6lYM?f@Q^8iq`=17ngn^{7_dqp&0S~Io6xc;F_90( zbAG8RpBfk-hlU+q8H2P1-WP$Wbjo4B{M^Yx{QO>*nQaNDD8D?7odOIN4q&={{iyQY50jGl9h5}}b}U)t`U@(jHRQnx704baQ=YtG=rc_4 z*3*EcOt0!tUARYre5ywA0K~Ho-`^iA-Zy`FK8ZX48uj+LmNqtArW@wN{=UI9NlpRc zPYY%n@|Ogn9DGV$I~n#d9q#Ry?Pa7@!(T`OrH)L5>;RPH(Z1pK^HkJ>%aLSVftz!t z8=a1}pab@pe>=V?MJ^HRA7!u8lPjo)%W!ffKevnlzae+4iRg|Ujp(P zc%^Moq5m}?pM;B;;wwrtza7@2{Sj+P0^*_k)K_Zs+DSBj&@ZKNSLNwibsi8m{_ZaT zm8O&gAXc)`KZK`q4$W-Cim>{Vgr25HPz6x*Qjks##_?h!oQvaA3afq zJ_x&9w%k&=*t@v}+s(KSGOT=RPq#zGx$Unw4zPRO`TD;zKwR21d&XT!(qw-vE^>jC zRKm;Sot&JwqE|-*OMJc8siANfg4Y4!MIcU3#w8^HwIAgfwy1-W9M3N)>cOmRUpOQJ z*>mwefFbVGmu{c!wsFzd)zZ zPux4ysVGp-w%xpYc9_}bbV&PpnHjXOzt0RwPD(<3J(Hc@G3{{^Tn^IuSK70sdi$if z9HCBTgR0OAvqK*D-8t!l62I#AF>5>6`g^)r?I;1+s3>!#BPtNgLe1h%G;>WC4>L2_ z_NVHG)yYEh9adHk-lDQfNg`^$i#<$&#_;j3fk4hyZmlgX9}y+LTjt~odL9Y`1nEI} z*|6Z?Go|PSXmlhP^x*POD6HjrM+5b_P430CO#KMnw0oic4YR|vKlI>g)QQ^Z?|cas z|EEa24=ooBeo8Lh*EVc_yqam7Wtr$zV{Y~0C2XZ;#R_5+5B$-6>nDUvu{PE=!3Z?! zu1c7m^DYUAYvJnCdexrH;XDZ+P73|8uVgJY)oy)&LHs%WU_^_9pd;$m2|~I=jH>mC ze-^HMr=pm(Jy^BPy>X{uwqe?T+GE-s=N`UyXHbK>`pN1064Uv=*@^HY3523eCw_O* z3U8UrD?sN)2j~6PE-l+9}t?HgwTL$b( z#eO&o%F~kRC7u}4aN|UK_suQ~I>Q}n-j zg)m@|FDICMB3;|GRlZCN1cilzrt{8vE>-UtGh!T7CK>^+D1FPx)AIEz8AddBDU$>>=XR1B{d_-k z*Q{ge=kNI(cZLRZG>LLz-{oo%6k?Sid22*FVb58FgGw)!PDa{&sV$&{YhXpS()$ z#vsJzowNQ3gb_fC!c)tzbEzxUTxW{9s{y5)5Gpwn6)&+mdhTyzl=aFX zjRq@SS9ey1PoE7tyl5jibdFWlWuE4Kg-`n$Rnp$8tA*lLn$p6#Efw8+?KK8D#z@%C zv8nqOZk!5vc2o~TLKz_2T$CPI!fGL)Q@hQQdDG30lax z7m?Dt?6XquETi@Q4B9Vh&q&NmPpq!Ww_HG`hOc=)W{v`@i==9(kwF>NQa2})4?op# zNToDS@8HFv$9g8HbVh>S?ukl)4Bdoz?-LR##_1s3a?emjoOAlh*{C?}el8u^)9nJc0vfgC!)J{pVpxYDeza~3 zHhwMcX>)`F;(xOMFh>LeBSg{DFGdLFG_A2@NI`V?CbKf5E011{DfG|3OtG`$QYZ}Feb-lI5#?#A%jcwo3&w7yUJw~NV> z4!#u3{pbvCO{daUC2q4OFqF)<&_MKVhKvrH6c0h>nC8!Ri6S=t$Q5D{{YRnKF7;)m z+V9isQ>Inl;5y~^iW&85q@R5}RaeAazo_Y~;>M^xAE-P+_3dF>;++jtdzF59y8 zf1T~XL4WmRysPEmy~6ZD!n^*I(VVT&B@bN8h8*mbE3-QKB|9I zA1;;>d?`81nP8+{jPWwuidr@!5DwXYnKY9)~}`7Q$=Q}GOMe9{LHEGuridZ<5@N;qJ!*x z96_A1Bf>pfo7o^anK7Yv4pS4f;zc$aC2#5Lp)%j&Vvgyow`wID_Uam6i+m3{I zKeGc$9Bd@_@|Y?j??0CKx~M2v{VPf2lr z8+xRiT0)Oc1R;cb$v}D|FKPH*_Fc_9TLOXf)4;&X8EiD&C!hPlo{+DeRc$Cp^A=r- z{zNK+5N#%$s(9}uNX0)XZg$82j#{mtT)6=|8=KhS6B>>(SGG}JXAdh-sp~GMEA}ZN zDO>`4+?^o?4)SMSCqj@BJT1;D$RvV*I=0*$JWyn7V|`NGO7qadA?LrfGYL#KU%ovs z#tYFzZ5j5a_&X3fBe}o?ng(3aJpynk325-?zENd8`U7bn+e3j>K&oI_vw5KOP^kAm zUrB<9loYstzR?N=X?-E*lhv;1wHvnQ;n3_U=jU#699}P6jRgF(`eB}5%wtqfcISDa zCihEb+5;w=AW-k?1AE-8%?nxV=JQW9D(BjW`H;g0P#bqyfd<=jg@l*x!P9uV* z4AMOvUlUfwk}Lr20+) zkncLa#Yzaj?Kx8JdMEI$Ai(~74(GPSVx4!JO<^`R)-hmIf&296=qUUfkm3dbwb^hT z08pylQ@7VfBi?$oFjzy)RXgo5CTXUln!8G6^|X>8D;ul8To;4Q9Tk6k{mY^5yN$Fne<`iFoO$T*@Ys~)m4QiUs*fmC?*)3Q)0LoTJI_r}kdD(*FA|eX zZ8rLDvT4X2`QO+_5P%o&PB>gg`Ue5w2;6(wLU??HFk*1~-#!eMF{lPZx0?To27fBF zth`!id<>B`gDF`F4GNK%j-z0(EdE7K;-@hF_Q4tBmL9g<)1sC=pFy*~74($75;oO@-l{;C-hlvS1P ziu@3;2N`+tqP>!0+K%6%f)HeFWy`hH`PYby%THLcUxwB>F-3DORG1Zq{7+~y#e;12n zUPsY95PVjKfb3*f=Yu)|9uK4b07~SIcwTO<_~F2Y*ZhgQ9`bl+a^A2bIs9_D%B{Id zQ9=H4WG|EohMP6$tdhm~wJR&P8tc3-MT}eLS4{cO#=eCwF@IqO+tWP({qkrOZ=R4D zgR7AZw_QAl-anXPfwr8?;b0~e@8A!vjy|+~%mlvv8{P50=)c(je8KuL?CSphr!Cfe z!y;{J0%%_k>}lw50SC}xC@Py&Bi)M=ttS2Mrv%Tsu+Negln!EV-iYt(8#thc;>XO>K4yjjD-)+#tmsl<0L zDp_p-h-)uIJkD?Bj6v1tG`N&h59DPix$;3QauLI22Kadk!A9gfu2Exe* zJ0$k^Yk?L5ZVnEDnPcFNxSf8XmJzFcGA^zqAn{{X?lW}n-aYEdKg~nSAP85MpqRs` zFYGN`ah%kXnN)1<-Se2~9T^R3JA~W2ET5r>GVF)gTgbJ-be|fl;^&P|j2DTcP1u*8 zSr*rhAYXGu4WRuI4;NZqto0&8APJQv=X^!-OsHm z92pRAm=WUv%c3?`Wv&!vQ21tnW#eQf{y$6UMAi2d$BJCCPUge&S#YIB40|<4!MQ$d zZ8mfxu6}{R-nCYDdzded>M`;~zo*o0-MaOFdhofgiuVuAavw~ki$ZK52EzmikiXb6 z);UoLevSnGOvT>8-X?CLTZ0$!>qSM}kf5zMFF~H(-YfI>;ba(0auSV+pI@M1ao-Q2 zjPLgzw<&J_whAu$L(*=Z5I=xwQZx3i`{eSf7i{Eef>llqjv3bq3AYS|720FWf zugvV;a_v*cKm76A2UigiPAPV;`i9Ba(cUwX#OPzsf5<*IPYK1&M{M(WFoAQ-@_Pf+ zOPFeLI`Fzz<(quSsirChHX<;AjSZOF>+N=++Nu?P6DqE`_S}h=s6!KQ{XX^MB2J3r zd*{RknOYVAKwdrH?{b@`YTLn7xS$ZQ;&v{~kY4U-C1y<06rxJ>_7$mW;>S&}nFN@S z&;(L>6@iLl?r5erybs~xFK`vs=M?5I;XjQd!LzD@AyEF*tI6Rn%a-rrx`^NV-M=VT zh{lsg=%o<729quODqhC46|TgfWM;R{c3-YN!v`U_ppsIf0-{07r~;`w<%0q)FJz(D z7oWb+zk+gtv&flQ(HP)a968+t^FoO}NXchCzQ^@SZvBe=%uNP+|BEmMNQ%u)kSWdu zW^ZqY99sUupaEkmsbcAz`QQkzWwDEryL}q;dDxiz12rZ;=fX35LeFsG&eP=w`<|(* z+|UEs-SCAqORU&{m^sK^hSPD@@0lwOP)w*ZAxdQ`99f~Y#M4B#UHN)WP0N+xNJ z>Z1r>o|^!#M7CgzDh%0`nB@Ageu7JJ(~}Pr6k8^sVB!fIO2Py{1^qlXpCnR6pa&U- zJI9I%#a}^-bjVY5*x@?Gn$O=Crb?(*DT6r6SC)E&5G3`M6>(~xBIsGExxQFNsggV$ zca7B6{MB~Slz9)*Ewrj71@|eva5nz(Hpm|@!xJV;MMX}=^Hu{YO%Rb9u&Mdao=YxM zmw*#AVTnT%6JUQ34_4J@IZB9oUhr}m&W_&7e^{`*$kDO*^RNtUTZ)Zr+Gx%>gh~_3 zJewuaPUN91pB66O=xsH0JY-3EH*pxc~h&E=O4*)6nF;4_nd0(Fx1j zgJYaxL~@=C`=n1c%@m&~;_Y%!=WY36gv>RMj_$0LjbG{YD+#R&)TsoWEH7R@mI}T} zG7k%%tEM=f&;n^q%zwJ;1$|RKmnfd=yXdr1t?wM5{z(mqj4qI^$DbX96wC89y0G>& zG(I=b9zt2@&ry zVI-AN=U&lJk$qAtAhDCgInB%x0gmL~_U5I4GQu6=ERlg!$bi`4boO_O0+r34^8)|mGw;&`Ab6aM?TjF#5D zYjt!`tBs`3P>`buH*E}tVNvvuT$)bgs@KNKw_9dUmIw4vCcfo7kPo(BMA4!D1Bqm! zpcMbfL)b?eWD@spK2M{rUsd(?s5wc5Afwvv9a5<4G%*mr_qok=c2_zkgVThel9Mkf z>hJAea;XTGqQgQy;nlxAS};f1o?u(WOD~=Bz59@N5_05k$>0>zZxmd=2E5*imG;GDy;h=o+?~K`gNvR`bUy7k z{+`YftTch)y@a}IPs=RYVgy@ABFL=xvg4%wC3zLZVi+U`-R<=0Pnyd;nYa!#xZhFM z2lC;&-8Z|kRqmJ)4+@&3i;Uy6ABDlTn}_sFD-c6ds;=xnW+b{OiDi6g>QRclW*VHO z_iIHIiDYIvBqeUdmzmU%TLVPcIEpZFB$gsdDq5=Q`93seOhF$ zB^H0VbSbfJsnz~Ws?$)OMiFKtrI=KJYI+kjXEW;x< zXsz0YtfHjiA4Wi8Lzo1-BvK9!ec*1!-S~j{FHuw{^IsBBnVC$`pD!8#TZNy?d$fuk z3=$C))h)l7zPT37E_E@^ga==YyhNS;8R3StHMfy+D!i;(3gK%D*vm5t+?r!2n5nEL z*}vFpzZEfvq$7X4mqT4*Yin&%X+168&!#56Lu>Zw=Zkm&lSRI~yLY|P2vZ}r&mH*& z3+V=4%;QAi)gq#U#qMcXZ19R*AsTM^qB^S-5^3d4)fws*5&5-#O$|j1VpByFQ{&6s z==de*1o^1(4VE$jQX_9MhyIxXR|)G*S*!p4M4+w{!VmKM(;r-|+S1m=qcPu|znBp~ z2^Tju%2UJs_A-GOjn#UL9~==_xLFsA^q~!kFfQdK=|7o3r~9om1Pd!GNtmETN1wCL z+XuBzPN~#jk{_k3C*6j_UxPDlX<-5X)U}cgtdH|A$4+MF4lnh(dS%gqS_1zPf*1ph z7kyNUvlu|yf{;koJr-Y@?)`ro#u`Px%VTqQkP4w*{v{B_HjIh3o-(EW4g#xQ{Eq5Bk7(_$!JTBNFO$q9w>~teXe&}`uTCSz> zb5@9LPJ7OBpP>XOU0{!mkc$=i-EZRhSpueB8%84$b!j9=} z;rd}IKc#wskBUZ|`~^VYe0p?lR)rQNQ&ENvKf)uggAPZS6*-T2OEWL%{) z1K1@R0+xTf=tYKWB?)5OCYaPA6x?Dw(9F*dv=X8Z!_g0f!4j&#z&W}Oqa65RWSi}P zMQxG-1m74?^T*%QpAcAC`GlPmXV0l}+GvPP?*-xVcDeHLO^1KdR0M%|k1MbsEouZj z@5jvV3pc%OiFPK@Zpfy7@%7aMrkem;xJS{SGAYq6NeMxT;^Dmd_Yr7X4~;G6gH|%m zgP`qT=Hhk)D8)U2_xFDWU-`zsRVw<4xfVRSldz}0R3#>l63J-um$e*efvi5B3803= z#a+b-ot7z_-_JT~&=%bC9zzAz*WT=z`p1<6=xe?)BX6;rk;A$S<#RL_*|v~>U{?Kz z?nx)fQzk`>QchQ{zkAL#`Spp^w+6}h^C-0Ktb!EC!h^-uPB_}c=nSV$jQaB!;$J(a zeo+e>OS@}%tU?#77f0i^zxWJ6Q;Z(jCPh90$(ehrZ&rELj#Y;Z@5YO<($ZFPvu3jh zp$Hg63CEoSEX#<KczYb&Oh&og`M=bA7fL6 zP=jhdvfpgqHWc|3sm34r-vYYHwA5-n+-YezLQV`G1PQ)kK4TgHDc#-;48vqDjZVl2 z{%(%lL~?Uz)Wite`dz7<*Gu8s;peeOR~INH_1!tJ^XVE z2L^nKB1OA;;B)AY=>bqPaY*Xw?dts3eT%LY;)(yfZvu4#osM3$_o}?jrOtY z|5skH=fD7GJ4^MLag?n3v$3HCUH4%DK1NH5x;S zTC$YxJ3b5CVsLHI4rb2FMSkufy^u2__{PT)`=FysBcj(#8CZ*SOPayq64Mi!4B#N$Xr!pJXJdu!(fy*h2Y7J4bNV zF+qLOtwL@iqGU1JaIsxdO+i`0s-dJJ^=nCHrCc5voBqFXe%c8Sykuo^Q|Irr#_`$g zk-m9O8PudMT~%k|)>R}U=7G&@eJL12S3cQ1QhcwT-&UJWJB^>>+>ab$kmCH?SIS^5 zzcujF${70NX&HJmgqaACKC?K67f1GF@Ec34;6&PJA%jt9avcR4e(@#ZRwJ^WV(gm} zpT2hY@2x4a_MfTB4rgY)&itLGn~wB6{D#VYT|3Z)l35@!_PSvLacJzXn8m}^O@Y5= z#S4Af=}kfOCQ9%s>%1}F!6G{l2g(3 z1KcIG%jjt8-yfa9KJBV5<|)adCyGlbtORkCfR#i7lw2kG2MLQ}I60}9=Nl!lw_T|G z`6tp&6Tt<~qW_=@vH&uVVl)i&S+>1s(<@i7qJ0%T`qWx6)B5183etVt~ z<L$+OA`snoCTt{G(XubJAwuB3ktE{9C9#2Rz9Np!Fm*?~m zx6-xU2uLnd|ss_3v@2VsQ zHd{tg|ER0e>I;54p6RgP*<4NEKYMfMEa{CLXh7O=&X{-*e-<84*}A$IrC8pi`*l9N zJ;sJi%g>H%6AOvfB^8mJSf9rI{`tIBVm06XcVEDZH2S@-A48IreRMPmQJ-=q}i_b~!l^M%+j zfxM%3fR0}YY4sO?BG7b14L`73cS>FVu7}qT41~ z+t)9ct?h6)b|rhu77Ye+B32E^UD^FKv_Q;{8aMH8qOn{={C+20y-Jda1%sdebzgl= z=L=S3V=z^91Yxk1yX1sQDUohfPnPEhQTyFPcfP3)uDy~E3S*(n`?D$GNQ4olEO5F# z?dU^+G7ik7BU0rw*bi67iR}DP-<$b3_zs#{S=4E)u9%26txB3Slb}wPiF#V{OCwBb z&u9PWM%7!ycB2LjhER!aT&_!b?o>aEE`q^p+O*GrJ)a$|px9nsLlhZs^Vh}HO7qa- z0Zf>)N#j%`jffqJeFAOP!D6eB`q2pxIHFZf)gF_Zr+XA72KlY(IQX41_c}_KG(xgs zEBu8<(LPBrqv~9?u*V}}RMR$7^eFh}Sn|EOvomt?Egh!o(-)DtGHxiDmNRC@-eR2tnkKie{Z0`D2E6TU_^l-y=L1&j0_i096_WIwGmmp_n9tq^^WF z8oA;TnHt56YTlAHoSn7|F?BZi89zYiB_#EMCc_c z>3bi*h`2jPC?8l_UVORZq|vY;@zC(L+eDt(#(XO_3PlT0(gUdX${J~X;h6nuT*0oj zsy1HNqsnaQg@)jv#6!lPH0SAZZ&Zoxx->-dv+0olk)9%e%Av~J^xmLRw$~)j-uQ@B zd_!dVAPRmd^76i5-gC`iaiA9e56B|=2@Ox(D94xfIl>%`50^%@U{j6LEN?R@O^Q`X zR6MQ3A~mL}wYyNjWXDqnXB9Bh_}2#Ty#@pAq9#tMAbG3lmQK`Hc(xNGAYu$js9X`g zEKfJY>i?$s?cNW0t-G&d%AFHQBuYsH&6oc64nU=m0>&*P)kEz`C(M;KS#bI_wR?oS z%k3<$XZIk^9im-yj>(jBSYbu@OMx8Z=vTDRuCL@nxjKirYPR{#iAtI_pIP}=$iPq| z%}Z}B_y?(dzoEE;U=~$U7+t$zIE``<^!@xXZBl-m>P$qK=a7>C7Z!dafoZ zYOg-*t?mO-2*%LJA7TZ}%Ys6=bgz4azLY*&u$fBvDe>d%KZ>@@>)vXg+5XXaC3e!6 z(6-QA1o`~tnyKK@?%!^tcVnHV(<50~!_iN|maN0`YYCZVk*oHVx`LoH4ZCe$RE4I8 zU#%oA*k#uCt!M0-c;tdO`#UT78zt=z)^p}gSW#*(~yQ@Rk~3Wa{+ zqTpy_|9s!SLcDUghBKA#6Xm`NjY?1##u#itWagKu$c|Gc!O_tHY zmxp6v7Z(#o`VGfj8R`4Y=hVSitU8(baCK(TktuoWCF#fL>OWCcBadoJrHY*FUx5jK zn^XXMzTL;Z zWMyT=y8Is*e(QJE6Oz?PyPDg;+)QHt{9tJ%GvJ83t9HhT$dK+K^p_oc+%p5s#&)lh z9s^&TX>5S{2Un4{l{JGkUwWSdZvoV^K?Q$gDI2kF>u3TA{}wD zgV|_uTHrel+9o<;2AR+)*&L1PxxhJuux}*snAjw36YYEvV9f&VeFFQ&vxL0C+%CG| zpCbIS{4gwOOe1AtvjhB^CeUvdWo~=lo`@iuTusti!pmi)Q9pIo{Tq0pBs@7cx=019 zh%Hd!`r-t1h|#xxSa03g3Qs{M`YL0mI&+xWQv9-x`ibQHdfSXj8A&$1cYQat#!qJO1~KX`|R+aum@{Z)>{ z=;YBSzd*-?g;QWeSCn)23U<;jNRsTsQ9Pzmk1VIF|D8tPMQ@Vt{{8#cta3OAZPj3| z5P0l_09gra;V%q8_20We4Nm#_9w|glrcMP8zCfI>m)DY~V(bN|=~RFyJ}s8dkNnF3ZBhmV?0emYX9ZXhdaB#ZR z$kC0MT@Napbz{%K3o*@L^(=JZzeXm^gf~p2vNKi7?_f1yBL*_KzaPz_wgbjGS?t?e zabJxg++$;3m2uHaA28Q{uU8ceMiovI!T+J5p{0MLqxn^{KYq{>%uB*LF#x%ygH-y& z^{34 zU8V@+iQ6iivPgLHAB>Hl>%nJDwL8j>I?*Ci|@|E(^nYpyAEXao*)8{ym z&nKBye7Ec|^BXkUh27!b$KpE1ErR*x(}HVyAn;z$)shK!*gtT`F3tsMj2~|TzOL7w z-&1PER8YLBcc#cnFiM#02-CK+A|LqtIkKpGwA1h7z}tM;E8P)#fSLEV3q>w%7gV@}FJ^4IMW) zBKtQdw$%Ro{Sqs|SeKSx^ok40bsYOPrz-Z)QNvsvZaz$7aN`r(>nbzuHe@A<2$Zjl zjCw{K46GO_{j)B9IV*qazqpX)+{t@5d8lnR!4&iLaORdZ*djoa$Ty~Id%C{^965r zIsO2;w*>ZPl|N^TeddtxqJbnn9R{fnwP8xMzr0$jG}IjPk{L79Bmo5#QqTtHN;3Zm zZK@jIB1MqQ0I0VFq{Qw;-F|e}li@Fd1E1;C{V$2%>HK_rb+lPGjCSZb>0lIjsckfm zifPc;%7g=zNN95W!S*C%lH-KRcE^jn^2Wc5U!1{h7nHicCk>b>Fak8hT^b$eSZZEC z4Nh$6b@?9RTTG`sVU#F2e|G7g?~xJvJ&r;esZF0BosP2InyDBlOi_qeRwRXaOi;K4 z*yBZdll~n>azh`gF+*rx4SZ~A7$fmZ)~8_5*6tjs<5OH^eF9{KnRWud2Y+%ie>>Ic zD;XSo%nYL``?GE?PN_01{=})dj7BtE4PM~Eph< zem`T}_F>H`@xzHvu)O)^0Xu|-p^ZJ- zSZIj|`in>5VSCYcIv`@(6MFde@g~W-Bb$@pxw99KPOpc8rQcbmeYm=o`Bs$u>pRve zrTv{QoOs(WRRabmrt3`P_sD8oJH27*6oU(F-$9v#j>jOg-dczR3yCchP6HeaSP@9TC8A&&`1sLk$q!9mb-x?mp@a?hpWFX5&`Y_BkltcQ^VP#j|)##LYqTgDX4KmuR9583??2Nx}IS=5wsw&`{ri{U+q{ z&|{k&GF!h>4u$%R`jSSH-Fa3wVvj&>X>~BodX>z+Lt7D7QN9~5O?x137h5@jBX#h6 zuW-v@VTwNDb)7ifTJ%Sbym$wl9;;Q9ed+Snsv-)V;t|VkKJDdc1&{4JnOn|F_27j9 z&&R4>@34E!YUwl9mqtUCHZwg+i7aWWW?mnqvA#K~#n%Kpy1AgM3U^+$7uot}qrxCeZE?t*E_YRXd(QwmRTQJd{ce>O3e?7BxwO+A@Ep&Ca z1zNbMt>-IDqtG97thTbyZ0CVU_qWp2$U7bzaaHxn3{tJnM|O_wN}= zvfTXc{QE`JvK5;9Gx38wKtLwUR~Ytz2l}Va(Uk zOSp-a^c=~i5Hp8JL`cs?qI0@G1`RvQ+*_rHe!XS}@I=UZ^gox<>8~p#g;4q^C^QqJ ziZ>(x7#>iRvjke^=cCYu=AxB!go?Arg%7Ye$50+wE(63EIzs&UZt$ZJekkXrojkRFj%8rHC8*;t{Djv9h{Zf1I&bwVC_uxw8#YjTs{eRSuu{ z;kUO4`6m2Co$8y=GnkR^8!_+WmfKy;Q{Fbn1=6`&D45+S&zZYlCi?5A+J~V~^T!6Z z61V)`H{t67+_`eVdxlWnXyChLy9dzAQp6}z4%Xk#O9=(~T>!@;UwzHfexD{Aq&Wok zP)^S47+-2MD%8MJh((#yQ4HggXmh_Z?uRZx>6X%4zyGTS^m)CH$@q^2c8AH11$Q@0 zT-fHx6i$@-{erwgF*D~!e+%6qgA)@ZIiJjD4=*={KdJVHl2IEj=fCDs;$O(JwJkH! zoacrN8^`=|HNRn@J5`NkMON>hvL#S4MKe2tE2hNMo({>mF_8=Umf4NXyn(6e_i8HeCjA^nF`K&9woYJiu@k6&z{>y zz95EU&}Cy|2^+?PKML8tyJ#T_$f!Ai$=t+#UP$K=thKjI52HT92r0Rd+y2{9Pfx-n*#6kWD?eztV?K z6$o0`eZN`*DC+(d%1|bBN6Z(=k*S{h;1u|`{HN<@Ib%{_4tPrzZdvjK?F%+!!PZ6Y ze^KJxUR=CG04AXT#j+bfZ@UUWW8@PO!}yaWya9sBiup~Ng~H9GlG3`>(ZTZ?J-Qev^gU0lvt75+1z+JM!w;>>|VRy;zJ7j>Tf_XS^SF9Doun0h9pe#*<>$)ktO+JcvZoR*Y}` zpOA|aQCnG=M}*~I!8HX3r0GD3@O&|GR)2KDG2uUd62!fz72~}V_%nunYY-((krB~H z=uhy(*?)SN5=!L9wM-;p#23)`X5#VdmpRhDCmvj>Hn3-fz4`dg{tkjtXGwQI#Fzx% z18eBNPXn37AG@hJm$hB`2k>%_DomnTat2oJ9(QO1u7QDt>y@VFQDQvOe&!8b9LzYp zBB6{`ICec(p8H$wsfO=9zusr)0$6BtD}12&Bnm_HVrLFao=rJ7G;L>IA2Q`7gy+Y4JE6qyai%Z30X?@EVn}!H>Zrh{ra6x@*v4aI+i}bg{ zqoW9*5~ny~z8G?PrJ&LQ3LzIT*uQgHxS~(U&JG0TBAJ%5+;{KZS;}3tEZ1|b3CA5; zhzGn`y4n4A6wUL{i2B02Q0uHBVx`sPcZTF7X33|6-Lt7TF!R-IIP}$^dS;A9DD6Tc zIC73v{X$2ey{g-z8*5?$hNu@hrI3HBANqux9Zk-LSBu9=K5ost z?@s{cSY-*=p{V!{})q?Vp&N53MF_l2r-c{#=y!5~uH zka934rS>r^nc`4cP}C{^jO9Ziup3zCemwU!?p@w^Ot;0Ax$5tR+UWmzPNq0qUR;wc ziUFv|1E|~wp3|&twsK1tnU_ABG+WKVTS3f1fT+?>zCvArk6{-F!QYr?x`xiCAnD;J zi5QY75jTl!&w`udwq_llbYlpZUz1fA=nvq)QKeVQxH2n^J)NXq+dE9d`xSnk^ch-V=LTnGQ91}wTub4J zx$>N%T(PutkuMh+{xN*geC(L^tj!HEze1t*rv04(n?jSbB~yTZ$FRqpoXALPo`Ntm zdRAFQudt#B5=bq2NoDhM_CfFIXM2T~_{!;Rt@hB?7WZWXUWGayBfL+}g4;W~TdxZB zVwMHOQu+0MZ>~ZA-iL?1uo2u#w&U*AcCW+T8=O0!yB0PFnKH5hJ`FrFT;*60=ZUogdQA`# zU*?~)#OPNYl)jtpdC$xyZn7yU{js6=06&NRtGq>(`Wv5t{(?`av#0piAcTOs7%Z$h zc?Cdjb;dCl(Vc2*9T-kGmlZwRq6VEI)flR|)ZZ+Tt+f8`Hbx>UW3sQ1iXQq(9Yfwg zyv$NMMfvO0YNuAx!o7PYTTxVu0$zakbID56;Ury6K?0=uOJZHYqFOX65hKy}=yx9= zqOAYdlwXvAJ(3>j%$Y8WJ!VeBp(K|zGEP=9;zdpBJadnIr!ncXX#RsWy?0slDU}UVrY}8Y(SqJ{z%{@Hgfe>tsyh46$(Sj_zNK-3J`g;)&R%jJr`7B)rw16lo}UBNmuGUXOj_L|v{`Zjs}}&!I@T zS^q~>RaRe1kXsg7>usH(q32j>TJnSjoaV%>ytk*|Jh#gDb9ir#SVA5!ScvpntQKT)X+Lb9Xp1TUU4lVK;yLDES;_K->l!i*D?z|nUnV*eyhSNdHJ3958r7o*G2MwtE{9FXO zN3k0Z*_PIJbzSjs(3xMLd$|8v7C-y5JK(RD4Ua9+#!a-fPd2X5Oq%;cnzvW7VW&vSROgN; z8E-hWAt{o1p*e}-r)t0*DW*GR>-56R#aAw(k=+yK)+fh7O3Z=kN5}2EXCfyCB+Ej1 zF&c)Hpm$kc`o%c%ymXt3Pg|uwR1Q(bNEKLNuU#2dr+pi5w`g@2n$zY>B?g+WL9<0I zzKB>wM691$i_Kg1FB0AA!_h!O)I;talq(^?u!?9kRo0d^>5246UH(2kqwOHDB|3A& zveWz{vH5@P|FzN0L~gxsOz@J`{Fo+BO)B%a;I|6r3U%V!KcS^4a^&#L<)2ANSIkf2 z#g;+uU`YpD3`WiCQ?_pTT{&oc-TAZA6<0!dVV`yf{H%rOzDZy0;8@dl=`Tw2GnY2d z9vLFDF#@1N#{Q!N{EoWMSShu9)I*8G=MqCsu9Xu2NaDbRX=-Q!@u@ceSwSP^>_uUHcSe_7pO z!O_bTfN+opc^YHtkX6Z_F*V+QVyVb?y;Vr&E^kCN=&df>-DPfk*6w5hFDos@;ot-% z8#hq+kP8ij37NY2n3Ggm>2|NPdT#2k(@|bXjF;~7`VVB9VM`Q$d+Q!^oS()@JQ{89RyNWHvwHZJY;}1gK@rJ= zKjjcH-3qFEPRhtIC<9UCYA8O=l;?&L;bcNi<|A;^k&rT6cOqS5~q($-`JaP{;o z8xzSxzkW~9Le3m<0F8$l8o$7=%`_$&oX^x}9Dqsf5-dj)&IO3c&3%IX zxS??zazC?O5vqjrl4H+cen(eiXkT}C+BmSyCo}lyCj3#*X{!3v`l+kR_J-&!4CheQ->%z4es*gglH*P>O zoCa*J!j#(V3JKLF8?mVZg(4p1oYl`>Egv3A(0Mf$u74Hue+C%A?=UaLsnN|@#@24s zm-c;4nnK{rc@o~M7}(D3|C{`Scabc?AnM%OM%QTu|EWQYG$o)pMhP>xzyg<CV0^O%a1--lHyzWE6pkG7HIOsaaKG)#hq@q@+xo4K-gJ zcdnzbtSFKw0wN%95}z=^mXm@;6QX7co^xhS^H5fa4)n#KabLF26)tD5eBc)#Av`*1 zh>SB(-#BE&C%792GL$%B`vdal?=bA_>{Xu}mT^V?y9*?+h^D@7&fp|kP-dtas=g0< zKuxt?U%DZa5S^0pcw+NGy~6dr!Zt$w^r!%Ra(3td-Y0jIHeJ!RE>hZ~E|<$bzjru6 zTq7jmqpY9U_v?%~yC$fb|*C35O0=sIKe;uAqj^inz)24AnY zC?kvDK?NR~2ObA%%Z7^Zg<6L%x08DklW}FbDi7L5 zu*RXGZ?QSqw}1vKjdYU%``Ugl|o{*98tI;Cu9y&K65PjtTE>1z4ae85kK z)y{0vJg$BSZ)<%i2ijbOEk{TqI|PDvq0_Pd=SDqWbssWxb;C;w9uWar-aGR)3GSYr z+Hi2i4-m{LoV1rt)ivZU;gX86n$HVDb5)R^?|*`e7k@BatIxnXOO8SJ)(SFH zC)7EEdU0SPIoDV%tkP5kUT5vI61LTEAkWWh3*mu!i|ottx{9Ha3|&an^v$x~YZLq& z7#M);UR|C;@x>+uc?TvYCZzs>-{}qCQX(`vaD9V~=O|L(0q?Y^41a%-^EVcspaGmz zT3^^Vdo5e!r?HK3b&8YI&Qb4%%iiXT-s*pPmYI+|DO)DNyyy)HXjDBQe)x7gE_SOf zz+l$N8&|Y_ej4~>df6)!o%XM)!6bcTesx9^7ENh8PT^89Go3OKD9;ZSEymhcKh=KM z0tnTeD~5&~Yj`sGxc$!)!elLw(;-UH!p?x2CgINd!`c~RP_g)@k8m)OFo3*#C3>#r z_m6C2k)WnH#m&o@orlel@5RoZmEVI?iY^GPP%JaLMBS5t>Ab$zQ7$QZEJ6yRQ(gx2 z3rNd;>M9^Pknnil^MaDb?>j|kI8R5Un#J4gd5;Jp*;*b5)hhm{o_l=)Dw*JxW6D}^ z>#TjwT4KUUhRe#JfCSYJR@St9(hjvosc6R&#sa~Rw9vk~{XE2ly9?ft1gUU3S4^Tt zwL&FUcDVH`^6B3txIu!k8WCJQHL{m==btp!6iwW z`;&p|jyA=y=FTTCDItLi9YxCb^EL>qTD+nNe9>`r4`CZ9>Z+f8OGlzQaF!noH>kyf zEJvcoD!Y=-ng)s@=|hk32QO#qvDvlm&)8Wk%8$3#@~1OS{>ZfjF+Uk3$RGPb6b5&P zxS2rbV(fXpH4>b@R6`oxjqRf7)O2M_k;(c9zmp@FcR~zPrz}x;C)XRcicyhMd`|Df?HiI-S-!x{{eh^d1rxx*7U5^N9C(ZFw9-*fo+P-jenA)za*%)=Yu!{i0@2ofv_ ztw8fvjS)z~MHH%oRA5;R&{fMq>k?JhVQsN3N%aFD37*>Bvd*nw zgnrBsL}&=|aRPQz-tw94s3?(TaewPuiFfjhC6bRauum%U6KP}OBb^<1c*?gMu-FlQ z5s23vDF$883dzq|{$ThtTO1kh;j}Dcg1K9v%yx1)g^>O*_+!Pz7AaHtW_wsC2Sh{m z_1!`wV?;ret<#j)e(6Ey`#1GX1szq9(gK8Bnq}>3=$r$HvvN(Z-FQwdSyLE7;=rW9 z;^eNHLzEK{ObTu_qw5MNvLRqkGT|%IDN!)S!5xYdRQ(? zZ4~$68)y^<^Mm8|2eKYLJFHMw!FMv^n~T*Trg~X82M)LiBILKM+~Rtzodxrrs*z2~*4gX(ecABFJ=I*vO0V6n z!Wxbj6Pzq+tz_k)8_J&SmpXVIC{vH#gU4Wmyvv6_4jqZOim+XLp;!I>V#TxQ0U2y1AS*lQ+3h5F+R0F|Y38Ft zA4WyuX-(!uLjph2CjN!isLW5*l?GcmO`~ZI#(y@D38h17Q--phE5vuVQ?v_ycOwR> zgKh#Wk(dM_W-9J3-1>~B!KkU_RLWqqjvuzz*rd0U8_L1PP6sJ0T0itI#)9>l6_)%t zQNs^y&!tCexqz&?1}U!+to|6*3<5LItfK3b@-~kE%%1@zPS4=BWT8q7zi<#YdC-#RaziHHVu*=r->-ueMSvE#> zcDWtWT#6N1!eW;Z=9+$In{tq~kuMvTKLR7cv?(K?OGVwM?a%X81pYpHB)-Xw0A)T! zsA#hus~RJdnVv2(3i73H0eHu;pxL|9fM?FuX9JpuQXJw)=^4vwS88`3Y7e^Uzquy@ z;>3W>U-;*GGN2_4wNB47i0`~bFf~Ni+UI?n@G0DJD}5-ij%H1G45T_ey$9_qo8)9; zqshh)S(QqgJWA^W8EgJ&tY|Fu*ZjQF9byFHeCRl6r-HFcu*F)w-9THAhkGH%6aaX* zbG(8MEc9)lQn{-Ue#=|j@JjsF^yTt${`R@O=-s>@!H0Z#Bgvm-gpnRZu9nf0me+@$ zwwj!z{u==fZQ7QSa)3YYPc=1Xzh}}Rv-D%*%*CRSJ|Q~#E;4xUKoHt=+4cnI1fZOdlCHplw&OR_kERl7R( zZgbw{Z>fElQ(vzySkQ8fZraw<(}RF`v@l}@Spiy@6v&qix?HCR{5_7d=x*g5lpbuP zZKR}o^y|RRkN7j|1E#8xN46nHw%M~E49|8#kI{Awc&aL<%3=`7qidPTUH6m#wclHr zWLPE#xy7M^1j^I18$9d*krfi?OoNZGd#4}*#Bt3)yMO^@K&rkq{bRp6T1 zkw5PO8o8zqxS(|I=yV$B2MCwa2>}omW%iPy{TA3JnwN#|JN$m7>n942sVnX&xtKcx zCxfE}A^#>nVyZS?yd>9Q(SuW3=FuOP6sFSBTK6P$noy^@SDMZqzqp>#iimAjA;_Yc zCL>e#+1BYRu}{6hm3JdW+LkPJzHM+|%miNE)MS-k{}VmKSOi9bFL|J5>>QEgG>}}- zM7D2eYiEm^a*D7!9$imD?yErS>+AIqMnz~)JWBDCEKbXR9AgFc#{7OIVOiKa+E;~s z3Lz;m@@`vlT#Hqd7rE2YPJqvtPIecoEd7_V-`EMwJ5acs=S-bIRuZAEFPcw;jWlO3 zw|p?C{0gU$m`nBRT?O}Jg_BSPjYtGL%(6lccWbzfn zS_L580|P0+R~U=_EFWZ_Utn7%vfKMfIAaIwHx^!E$_&ZIzUdA*-Fsy|28v=C11p)q zXGIOyjm^jGz#T=HbjpC{pzXltq7#?E1JV5D6@_cR{z~xG<{StIpYZTMk_%D=i#>${CA>d$b?VdKa5fv06#7NcIX$1-Ekv(^aCF|W(v zf#3v>;HwJ<#d~?gCF*^uceuFv>BY5FU4eI2AGx5v6k}s70XQKOYSVlEWiz`E`n=yM z3>`F^m-MlhH|E`x&}gN=@C^EK^JF zk!U6+lezEh&RF`c3S zgX~(#M9bDc1&QX!p?{KaPj~kZ9#kf`A)+X}b2K{JE<9!QhX zuEJOV$MUGCZ{O=0$bRwjL(NK_yeS4y#fR@Tu7@*00vXu7LQGUZO7YgDyUsUF{MNQQ!Tmp=w@t?jO{N1Nd@~-z+Ra683Lx@cwG!OoqyTJ&o@f zEg^2dC2B0yZG)fdc}vriytpM}+mW3_E4*P0WYNEujkvWCCIrQeFyRj~q=~6Xj4hk} zjJ_zyK1JXdZ0C#zkjS?vL9AG0ouO{ zuqtu7(R%$>blK<7VX#dyHZCp=$p>w~e`JsHMAzCOaX+Sl{+@r zM#xBW(^~(oQ;pZLQ_ZX8s9~;gl&tIQ#r>|+{~a|mzhEb4_3I`OVUr46ja~4Kj#TN8 zJ{U~l+ZFZHYh<1Ilik&bs)-i3dXl{)nSU?l?|n_5Gh(*UaSY3E+{^dXxRNDPf-!&Vsg3{P)&Fwio)5PfH1Lv`sYhz z8+{Qml2nvRyGF)93>GGxLwVzV%_?ksz%=WjnjfW=6=7XKmQ6t#IM^F`zauv$xVHR+ zBjq5YKQu+)2H#(m)|yvE1)pgq7>RX@+_@z5N1bLPZN?MTpk6~e6l>TX8kyKD#^KSN zOT#$!TBZ2|*j%n(3)e8mDDv83XH0qY!b$ALKY{_Lk+swlQKP?lHO;e$5qvqQ*^lK% z+=Or`fg2>!sj}i*Zd~dl@6d00-n^~!dPoe!_}MwJ?tGM&bK~TWvMa?7F+toAa7V(& z>+(u%ZdRd!Tk{(}4dx@71Y9&Gdpke*C276wUx?UKuglOQD2$&~qs;My@PF|8FYqwEH#m}wr-ZX^u{WlS=K zJ;4DnN{ICT&~%>RY(DPWkFCRq+FQ)D)Lu19YPD1;YL5~%Y8JJr6}3kZv$cg#C06WF zRM6Jmduvd&*K>b=$Nzbgml;Ryb)DCFe$Eicc6{XQbtcZN=zP zl$uI|>VeTJ>4ZstD-zM8FP0U`wUTFeQMJQN-bD=gT=3G)rvi1~kX~G%NQSqP+2>`r z8_dne{-fcRc9gGRh3-7uDE!nqcIz=wx8!eC5WSQ@G087gq|(x#pSr9~YZ4X5-}@GE z5^=>Xb~`t`xBd>vTpbBttdY3qFJJv$J(jM>cyX3n%tqFpw0X;)R+Bl{ALxiDlvUr)(>>K9hQ*V4>^q{9P(4zXC#Y>= zSaX)A)E1#^%nD=s;5L91kge`@UYzO-==hm4tEq|xJ2+2*jVMLdsRM5sl^gaJC*Nil z&L-Pr6-Rf{#X8ft>5b`by*$|Wa`UkoYSe^UFW+)`P6_2@@H%;92k!51Ojgtn3b>|W2K~XoBnyhuAohQbeqSqA%$Qj7N5Lu#- zP$O-$OJAXj&L;Wxc-gyr)a`I+5hI%YHEV%?W9Wkq#98zm?R4^H%g(V^2Oe_wuhOU# zyeHmt)vIl;PIdXSd%+VM<6Xq7A1u#%4lP4mAT60|^6&eQOr=OBWC~U8hk7i@vypC^ zS!N$95_9o8O+?^A7@kOU=%<0?SYvAPw$|*&vJXT^@Vn_4L>LXYWYQD`zybb>ls>s& z)CWAmxD0o(_MdtgOxiO+QJaKB(Lebck{x%)QJjSFGj|CIL8LrUW&SW0om!P6F(G{K+pSoJ( zf9uyayHG38x-uoP@+QGft&hcV`7bp1n4a-nJ9hF^w(YMK9S4i$$B`+5H_f>%ZRbG! z_=O+NU8SBP=wtli8}dlkxtKpK@SF6#`Mr7L$YYDP8?9;owVQOty=62!!Saz#J*(xF zw8heYcbCf>=35#orG^Gx(7^TC^1HsTdY_0BEgI(@Z9@&<^B&$OK*Cq-Q$A>^$bq!p zhV$ZkE{r?`&h_e7Nvc*O-BaXn+QE;$H?VjMm^a_8&f3{bvlxx{A2nvM7tJW}T>=WX z`Wd8VWr{{HK#`v3vLTmPbCe0`X{@fy(x`8gK6cocUcQfOygT!Iv0p-{7!)0GQ6EH?L``5_R4@E=6}m1 zf_6Oo-dK!`k$8SAbmfu ziQdlM1FVWxD_tLCM=GA{Xua z8JlGzD$e{*4e_Ge^HZ_Jo9xY^cL}-jYPAIgsydh3M^D#n=ehL`ENWS+;=wKdNamvN zacT(1w~r#KMNJfXz@8$j!GAzFzl-U~%`d6IX{-oF2hZZjgQai6KeS zv21-t^HFR5x$Xbxt7{ciQ8Q*H5@e(ey*<-pXu4!+{}|-5av&wDii(h zv#wu@&%1@WJ+?WrpG}poJBdj4z!@GnW%;N~uH|ni&|)ovG?GJJu5<`qGw0&t->0#?~6=lSwg)KIpcq>oZ-@G^e5%{LwZ$)ziwKELh{ zhQs-(@Z8I72d>vTL$c4J%V%lpfw5ThFd^vjwa?9eplCu6N5H>lFpxI-a^hw%m;}^z zfo*#V>kxkN0Cse^VfnhVS|E(2J<_is4DqBXz)-gdx}*SXnT zWb*Qh<95@~%%8N$^Z`4l`Qiy6M1tJ*HpB0EO=9n7Fr?$?Hh?wdlk=WKVb7twfD<`r zk9=26~hgadNkeS)_sD6TolKm%j6npEHBho*4|Hp#E~(oQ+s{ zH|BdTe^IJl{3mHdVPI8EPbk(|iUlMiBO?OfmYfVIT-L~*=VpdJ$nYJ(ftph*HT>Pa z(j=V)#BVlMp0_S|&9tT$=iYmf@&KlzeoLf&W++K2Yj8DI%oI0>Gml9dk+z5F8bs4n z8s6bK{kv0@(U>~{x!QhXpf%+^GfC03xzd|ptr)z2$^~h?xxPT1ALa#9>d1PM*Ed~E zon;DmVRd>AE5JzyApBycIMMBl2FH{KkWv)spuSdM36QH8#68i6>w2D)4>E%k-~SET zH3^F%2Gy8F8KS{qHKMBmYA-31>6%jZsnm?fXWb+ z4_MtJ_m2*%^b9P`%(z}JPlF>%bR|Kw!w@rb`oUzx(C~2FyZRl_;=zczVrkwPASp84 z5BTz!3?#cz?C@}?>RE?5qOfd3uU0X;Y#XQ%x@a}>GEgwXIsE&yI|n{ki%Eyo&dhT{ zGKt#%?t{P_!c_hSI-ih#rrE&@+2+(8@EX$vGXVYP1E@UEqN5|y_Bw2(#>x8f?Y_Lg zoq{Hd>2o0Nq7=0IzqFgDaq?L7)wl`RKhLw(6mAF~kEpDSWNNZzTxBvPbPiYOBV<7` zDRHtVGR8?Tn|`x3c05Z6k!DX!6l|O6>(of&5RAJ^KmI;en_F#!6t(GsDl0C(FZ?Ne z?_$VKz1w7f%4zRZ$MA@8DGF+Ou3}J@!aqF4@4eeld4z4q%gtkWi!sc4MuKOj!sA=y z;OD3Py(qtpAvObZS3?)ZNNT+r>pnD{kln;btiKDsJC!m@)#*nz>uonE<~8^)CnVoN zM{r8zAoj?8<_R~_u!KJhUHpw~e|O6hzUnUGv~%Q_Sv=@R=l}EsPE0>#X>Px&*NuC4 z3*~o%!2&PWf#TTU6N1qCi?FY;<)5&5xw)B3A_nWk_wkk8u)=j{Tv|n@LW9o)?#FRv z$W`qVjYlR;*_10Xih-Af|)iYIG^ER z4H+?ov6npQ<9arN_tJxfzeaHMe+)XdbRBqmcJnm)3U5hg&?a=VApGpjhx|n~qHHpv zg3wI0zWI&S&}t|$#GkLre!ew{JC(_fkGdx86QgpzmQFmeJ)w)PSjQwc(X(5GI+PwV zk|Zp80c^_6!gt_L)BnW|CB|C`OY_IkdjEwDQF@C-gi2t$8ee#X{8ZNZUbU*!mrYxf z21+>`)P50G5^_J37W9IIH8%uyXfDC;C6c{Wm9pXXA-ThTb^8tJq@ppmK#^_Vn^m)< z?CdutK__Xu?@e6>HVpN6#0@Z#RBR+Nx2)DFrSAyR@PAXTI+3|9(SqebXn#vbyd(Y^ zXQQ6mqM&@wvO41KQcSRB$mS=wD~%5a?+oPf$*Q0=IKUGuZO=DW$N3j7o%DXrQ{ zii)Zs{W!St37B3k-nRLDl!WvbF4ys ztT>3(@bmxe`}QKykHJaPzc|%!0ViR-w7<^tz9G;*Q6du1 zek!Y`I-Do!>X@XndFz%BB=-;XBlf|$!S?JXfx(v#z({{kl$?RUwjcx^ht$vI!nFtw z+H;AW#@dpB+`4<`S}+g@yb_h%s3XbF3$~*-T}_tzHxroFl)D#5pu)c3@BBh;vntQ% z@gvpe2`0k~u3?;)g{h;?eS(^^3JI?r$Y?xXl}X}1OJY7xG?4OCbDZHu=Zq)cvP-%T zHj=3oOwGplao~SU6O#iyR-Dhem*J(;FIS9lf$fscnGQ=d5gJw-nv3h#)ll|^)2j73`#B1A7R&Dy) zR=dN@+DB|TKQ6JBDJk&|xZW|!&>Zl`RMHJ;K7FbxQ+h|^BaOR`udb(!JNFW+`J<pJ&9z%Fv$gJ5OzH+IVBu^R-F&{R3NFOfCHAcu( zVZs^XuawYS(DDPC9dZM)`8 zTQP!~O5KS!enS&3#rOJ~cbB33!_LOxE-0@Q>7rCvYeJsu-TFlRoi@CBcw>Au@Uw-L zy&yJf84V&eU!F8YN0;jiNZ2GLeSEfmH&}-@wW1(T-`?ZBeLw6p0U7;`>19se5+UqR z?V?xS_Qg~0jchAJleg#tiog*hlPyYZRo0zf><1x9#)g6>3=(~8$G^{2HES*-DeVJ1 z9Z7%?)w-j4%$!SC;wggFySINQ%At7-$3k>rgci3ne;?@4y+*m35LW(TU{?I*>FMJ- zxp(5gm<>17Hj^!pAc&sCeWpjc8AKdDstQ3KEXM4wOBndDeII-Iv5-z&+Q+W=af6EZL3ZG5a0MO_E^wmo_a`E(qgE6uV$iQNrW&KW{k_ z6d$}5`w8=4*yVzNf0#;lJMdVzOc}`bEMrxDuygyw9D(L7J^Gn*|N=t>pGz zX4%1tK=dSf+F|++Kmy>fF-Irf4Gc1CaO5)&!~;G$4HKaHZ4%vIkqS`}4}CsJF!Hg- zM(E{Fa_JR`XDiVH8vz0&pu=W5E>lDu>U6i2Id=gT(*6)J(L|@BP&{>ibP4l$IFUSS!cItyCvUJKDJCIuC*<3l}UdGv#r9A zGDD}E_ik1iz+So5&+rhv#-UXe{*&>!=y>V!z~%oK(S$^g9hTMTPvbtK zJ%kxO;>4u6v&L+3>dib3N$uOVmW;>885m}>RSqux7}R|BS!7UN%9HW|Cx4dqJ=wds z^MgX83Lny7$pjDJcJj7SD9@+bF0bnga#7?~+au-dPtV#WCIY|`^m!?)kFz$R)Sq30 z7!eZvZID5?>?RN^afq8lM3XNSMo5B|guhDJXyC_m55kk%ckN3Y46Pn2?%0*;Q z2^DUh2{kTtCO(wzd8$IrDTms#EEl}RCk3jaVJ^`0@(uj&^Uvg;njE{`xs%1sdEj6w zU(B=oUnqDHzXObdCl_S^yN}v_H*00ztp>VVf>?2jMFs*9NoWzkrCy&3-(MR@ul?Hh z{J9QpS0tjv)4e1+0x@1<5Bj^jsJ;tDn?4wk_~hZ{qubITGis>8?GV7Zv^8>{XmrGe}h*5={un@T}z))OIzQRSYKks$A{zKq=LVK?YV%S z`kPRWQg$Ch2q16$Gi1x$|&CfJ>!&qS;AN#HYjQguLgzS@au>NwmgLa?R+YL4`P3 zn@rHXMslQ6p|7_bh+G2S)6-&7o`Sl65y!L(AgwdaE&do_Qf1%J{ieWMIK3Dfdlb{x zC2jFM+9xW|X{|qne?^8gCiu~AvBO7%wasw^cLZ<6B7U3|1Q0he@9T0?d3ar^WVwFt zcni+Sx(}x5V1&j@J@_E?i+|;nj0PcRbQkGVy1eCQIl{O%T_XBr8Heq#WqfCB<0I*g zm~oSU6vV^4HaFLcu6Z2umY;aQ&CH(2ML{pegvrE?q#TMi=1m=a10Zl&c;*^C?HMCej>?e6PPX&K%Mj#- zuNn+*G0T-n*PgOU{)!qL3S|vU3pYD)>akI#uwsn#2jbW7|Mibt1+~p*H#v>yRVeyi zcUB;oGG-A7sjO$pWsg1`U)L*%bcx)hq_p^TxG_v{y_)hIAy6VpTgtrt*^zb5>4i=z zkNCl8MQRuRipnu-SB)YJ0W;uG@RLw~A&&k&CPTUM^(%0u$N=UYQ*zUticnl2BX?rAldIy`d5$iZNt(hE!P9cIXd6H)LD&1zW4H| zL{+^7_`_=h42s5sOD3RC}HuGu-g#kLUdO9wByiXC2IeN3E!_sN3a!1vm2 zg0#v~RflAYxUOH0B`5_|m~b>->tYmr&ublg|Jl;DWD*12J(YVu%m37oF;`XzbL&DN z{N;l(@qjlY)DQnxy(8)(iQ_CO_4mZh>GKTV`aDp@I8n%4rTa40XOHmoyL78){7Oto z-rcVK-2-kP_3~oJaRjG|0+qxJv?Pms_gf-cjPV?g@P;Ze$q`RlXF<-RXrCEt>JY~t zuvB8aBqd*`6=c1bneR}6eFOhmi&TN~m3nE8N4(pHOFg!=m6Xs|Tfvt8P=p@c$LOTc zuxCzv>j%!5)OsnPK%>T@>Pa{WNYVGWVzg<&6?rz)Ln7Y0JzF86PkS1Br|v2U2nD~5 zw3Z3{(=pfGb+K^XjytwPVUh(uznAf7p5LZz| zbLPL-lBIXOm3Mu@n~*+%b!Qc4Y;UQN`}Nx3Xf;6JDe!XD;omc#g+G+#W844!NeH|_ zV*n>{8}a@+Lu-bf-$srPZ1w!S33I&DcD(~XKDnN6V;Pp%xKz3txj`atus7>%24lU? z6-mFcllfRy8L{h-!hjju;OI!2{bS$4xv5gJ=XK7)SxFK%w20Vs3*eg}Z|Rv{%`IG+ zgWC4*wC!J>9Ri0i%BB7y`w&1?t(7f3U$}znC%e`U zL{&~WjTOV)p4koK767gTsQGBz6#kuS+wMLbbzc@Z6$Gn{7Os!$5B-h+`rFq&|Alit zFx^P%NY}((OyBx0VY^T3VLIhAOoKm9bO!_`TY1Y7ZF~nt351+#gmbXvkHFysoS6#U z%j?8KSnNil2sm!#Ni4az0_*WQ8t{*P!o-TPd;?{Nvr1{F6j;2in)8x32LGy^6Rq+1 za45GI5G~twf|t%W6>CILM_*JN0e$p&@rN`ohPvQ2FS6=p(5Oo3EPyX2IOCA1c)A$2 zxLBO3^D=sKdHDlk-gkp7>7^D}3;QRH^X6hN7>)7vI@05zaxg0S5XU> z>(V_2C-->$uF%psB4Y#N(|L*)H4d4-9eMIo)rTDXC5<5lgI$jGd9%=ll?|Wgh&Lq@ z3I){I5su4_HZYaS_oe^*NKb6ds$L%T7xco9v)azGX3BT!=ik?0>Spj-(3J+#ugvN` z!%(2X4Y(AfdiZ&F;6`eFP8fufk&VW?6+!X!SktlWu1lv~?U$b%L>+wPK zoOW5uZQJ|2vEE)qmS;bVJcfpI6`n#JvW!Bk86&NA#}X9%0s;fC{;tvKo1n*IYiIUq zDiCilKUNxS)*Uht0^%7{z$n|T62%qWx8&w_GEP}75oINKheDMGK2%fwQ4Ub6pQM?O zVWc2RvT|N0DF-~?IOn)acBvqL=cp6`3?i!(v`Eg!lBL(Z#yEPF|9Kv`KS&zp%2+FeZA%Px#rR+Af1i1KP*UX+mc~2Ktjcqfxe_E>!lVyR@p&~1O#FV z+_v75`*Iw&z(F2Dy&fbT3vu5FBlw(6C`Jv@5{*{bYy;o_SMTeNcM`f zn@XZ|P0f-Sc`;e)N-f98uOijEn;1Wnsk%hItr_P=1-IK?*MSgzGjxQ#_0ajUXZ>`3 z2EHCDJ?AW?@}%;??&zXr6>tq_gc46IMUro^ZA`b?(m!B4Va->=GW z@1f)+#63kYrWiIF;G%3QL+$y!bC$tBS|c2`{{RoUe~<1}z7_2r=;xz*!jIG%YLsym zv{ZwZkl*Xg?@iuSSeAPEC4(qbfOo%>MYk)o6;q1<(gk5P?AII&mco6R3{En48m`#nY6 zG*Z2>dE|xtDY_sfo{`H> z0nbz8GSFq|5}KD#8tVcUjZS$&q-U-D#q%#xVN^eu{ycDMYYZ0^6_Xd3p|L;8W6 z6VWdwx~z^yR5o!)bw+>UnwyN`NIAnR@Fcj?(;A@CTnf zY~V)rC#vXQztm@fQG~h)j1vLj-&3SR!uweu$oI?{gNkg zZ+fo4NU(f(P!j2@m`2%w*DAR@W+*kO$;Q)G;bqHem%NZ@8t=&;J&J2Z^d_K?k z!?CVmj7FJYMXWR`pGXZk&m$ZBp}eo42N~yPy||^;pTF>dHM9y!Gu}(95Rx+d8y#UgPc{P+~>|)*fkeDqYo>t#3k2QcPE6|edQx&{= zLq6Xw3#mOaYVZn*;6tuNn3d%~5;tYq@FN+fp-Wi}#6QY2TjrpNUaz#Hf&wp`m z`b^Yfj^I2Hb8pG3N52M9Wmwnn3kwA?il^ytu%TBwl)_PxRX1ZQ>rZi}z`u~UxN_7N zdbKgLH@UVv!14!r5mXXrKRKf$MCk?^Wwx$-HX7WvFnpEmLLL+8%Ti_XVa3jHaQy|- zAk&UoEM?l%mWUzE5q+fHz;ZrX+gg3Al=gNc=3}`3VDm}LF|-H<&qO|1mfGO`4);-@ zx7lMAJF1%mnk;>1_CJ;zIyo9-P&`_lt%y{LZIC@{|NYr^up(I!Qm^;?`xmxjKx`m} zu+V>zl3;+7P|1Fgl5#C>a%PV=5_ptszZl^#z3{Jqi1vRXLj9WOgCz2fW?#qz6N7W! z$LW(euYDb6FXMOtQXZKw8AVlT%Wqk%1NK^N6@=fY4e75QdV_4YuU@mlx&BVw39MSS z-QPDniPi*I7f6! z8Lf{nM88ZCRPy_ulrXC$7A6IZ0YL}L5y&LYT0dqal-bC@i#vqxEeu8{^KCI+As->) zL{+RgS%+5XdgL-Qxp=vu$lK z>ypH1)EMEtX(d?=NPaW3ekPR9WN=PsA)6_`Y{j_Nk;|tG2Qdy;O;`3(2iVL$APweO z1&J~d4#7J~vyGnWJoW0&O|p;*sLhSx74n{g&{+jY9d1s({jRv-z;~`j|9uw17RIG! z9MjWq9Ud&d0hiimJ|_w@Q|^4_iV|yD@Nd~itaTaCjX}O9_lHG&BCwsJR1ZL68s3{1 zuaWb?myU0_x#hh;7Ki}X+zdlcmTm2Q-eiHA9(*=A4o+XC6!cGELw;3T$APuigvDR~ z#N@$novQe)t_?JC#DX-~__n)vgWPCe036-a(HS@R+{j5Ck?Y_@`aXFTKc*_%4YS3) z9+K4^8$yH|v#azn73om?0dU|WwLlYA1t#+WxDo&dOfP-4U#GRE)pc6iMir)ZR`V@u zhfc{;&Z)iQ)GZqEu7c)dXEzSb%cE2KKJ(b*KZ)pvo)>?N=KcR(01Go;myP!?SfVGB zPq!ZL`{ue=PhF-!Vqc9Cth#^Fn|7(cT{>pXIwsS4+{` zAg!*%-i6buYv7$-FTcBHOu61r-MmI0IayvXgRg#6UEUUt`ZjLVc5Wbkfw8>6FuFsF zEU(ure>3~W~O0w-Dvp_2{*P98~IjO$^JpwpRnx~umyRWM1 zS)Fs3xyRrtg!ln87L3&zQDJiajJEpLID*(L?b-vo4lKMM_L0AjDa6Pd1 z40CC@ckS?>tXJJp(iM-G_uD~>XU{3Lm15#oCNpz#fQb?eD=j5}V=(JczF$Sa6v;9h zR{5{=eTek->M2PzFuw_Tqq%fvXambIQraZ#^6}*05B&LjnDT114JA1_@dwGJm@aQD z`t2|dwKm^%feW}^M3!01 zvK!VKpt_mz@#&)@UT~7Er=RbYP0dtxg-)+~z|NmNqRo<^?FIV&;i`vAHP3bcMmQGl zjUAt!^Jh4CWKXaly3ew3LgGX97(M1^wSIA8!JTh%2PA|>`V0LEc}AQ4OL-(pgW*G2 z8VwL*n6OXQSe|e6j6!BvLca!{oox>s7`~vx!tqF=j~#Z&tbOpGi%vZ$Jp#ck;;MyC-lU`#KUMAimt@p?GS+H zkFV4}dZHsGRVM4v1jh$``|;$@<>gBld*DAV@s^`;mM#%i$_Qka_NtoCjLV=Ps}Iol_+(XNEL9H}xu+KelJvF8&;fsUBT%HKzvc#~3woa$e>FiP zHrI|SjavZE$L8>L)Xig?rsihbjura^V>>17DPHf!rZ=_(^k(JH#ABlogoJecXyJe* zA#h`JvtZHXS>BB=R48`>ASupW4PWn?RtuD*K79DlELyg4#5$84!A`?wIu??MNNLsr z0f&r`!PV-$?rV_^WNxk))+teofT&tTJVivVe`>wE0$J|Z(%k&+M6Sd>uxA!vn#|$9 z#wn`7{!O>gzcw_Wx(L*lt`ZSN^E`t+NE#HO`8DI(mZNvxn0Fy-jnEb7OXhTc`1ZrE zmoW&5k(e4eQu73su%LrKdr!kI_o)Jn!&{HZZhDW;JrpY2yN5`ljLYxQI{pwSs0N1X*|g!Z#3)rwLTDqwx&|k!`VQN!w3>hd3pj$V zSwsWgmZDp58-jSj^TgTOA)Ild`go$cjgm~y1AJs-Ss88obDO*?5Ej5?ubWOw1ajBU zXo&Ftk)4G+>Xix5jd#_3{h%??xeuIs+-fUB4w}yHd`y@+o3l7zZl!a2=`}FO20cl= zEBuG3gk*$T$oHvQBk-j>ff|}s*fS_ZA1@MyrCNDotFo$AXQj~38=#fX`ueTk`Kmrk zM^N#jYk7K}m*kSQB&&3xCLo{+yZvQEB+{gMRYgo3F-h|H!3(3unS(4c00Tk$1|C_edK-K#>F2LegdMsJN*u2Zv)Z{QJ@{lxTl7zYQBVy#K6lL8!y z+-+K`rG80}bLR>}a>vS1s4g(5J^eRFE&079N7M6=iASO}eVcb!%2dyAvEhEVqZs1j zvwWttpN~HI?3sP0CLLo?_&NTU_np7#n}`MKm$_DxxKfLghcNmOEeNEw`p526>U;sb zE`=pP?H|@39Ysxo!Zf8*b8AAnaqHj7bCZRr;eus|M$Ioe($!CW;*P0FXXBUAp?gvf zwpnc?5!=H++?n)Ix|lUrKr9sN|Hir;Gsw5P$Sj2C{^g>%9u`=eC48Cd56{AAmV$}A!)LkMu*59n_rN!&{ACE*zqbg{k1S6BEb<5Qx zRnaOL&&pXQOn5^R^#}()(3T>uxkGvA`r)>uMuer3yy@eP=)pPrGl&ly1@t3|34qus zMiUq1B@G^^&9Zj)8#AsFdy1j8&FMJB($-&&g*RlDgAv9w2Z9%rZtRhoGa?nms$Snh zWXW_TWFVp*uk#TvsJW)!D=F^X2}F-KO*!XpB&@vBV(JyW-ufj%5}*T@Q~n-v zB`Q$aD83o{6G?MN^ZvYEx9<>9hsze&4&lEzr?Fv zm69IpfO!*~hE#OaYy6;qm!N%%+DVHkYH;jufIL3MP25LaoGL(>pU>oH2Cc?y_UsF% zx5cW=1TpiB)~!R2qjSqp9Wq;0a;Qgc6_9nDXwYZz#Ww+T&)k9&3A&MrP z-jcB2O3E>3KO8Yrdnbw6wz}%I`>iff)^t?#yU}NEG9ui(-bR|@kZ|AQF$A5w-b%B` zo!9J$YeQ2{kfv7o_jdwMV51w7MLs>()M37wtC(GhXjC#Hk;z@m=xcis)>)7!Ej`F1 zn>N7f2MF)R?%)9j+^}aj*CS+fn~r)uo1MF z**TlDb@>_#>e+)P0r=L-529&v*jo3`y8~kAO?Am3tpH)rW#q&Uw zi$QaM^u31oo^{&-MY)#~di)!^x+@B`+N#8=Gc-)hNW)Jf9mbU~b?^A?GAuAbTKehd zhbrqYVbRuU@K(ZJeglktzkhEG1M2qg$v;$`gXZ)X?VE>RJpG&B#&3-(>3^;83l!gt zF%DcnH2Isvv1!q?xG;5&<-ooTY#(&DVe!7BiP#1Jhcr z>L?FTL<~5Zt4_Jk_+2r39V45|^gXIT)8D-glX=8SlB9hicV9|8mCBR;h(QA4O*9gR zL-TT>Nhl4>iv*!xB&{n=q6ATyygfk;<4`_n4|gNJvbA>HdK}yCc*VyWhczy#@X5Pp zDe0z3vp+%LM4_11QQH4Rfn4D(VskS#;Qz#Fr&}&$cRt**rnKTRkf{!iRPji0;3W2p zu-^e%(Zu`SByH=dg{|+`mE9-=3oqo=H@%- z`(M;6A^v1rh2Y*Q+!i%qGyjuU(7Q&MrCoYzo^#4Np{pB^P#XM zDemKBuV|y(#;Q*Cy$lpfhb(`T6s}rT((AR4D>E63HN5aHCm0kP2L82n-DGI~4yi?@ z!A402pJ%7USN3G1Gd}ihZ8;3dhGsOTE*Gv;n6!EeFypce8k~mQe-A*5+yhl$p~5TN zD$gPK4~^iKj|*{Oo5$M#+zFe5`aa_D=a#r@y}yNX3z0hgzf`QxFu|;>g8taR00UYQ zt!)r?2Pmx4*WxNXT>fi^KKlFj8xn1prD4+Wz}23PK@B$f%S7CBjJtPr3L5xp>J_^m ztJkvHvIw^dzQJdY-RcBCFIkh4;zdP3JglY+qdnq?wEbSxP&noNEtS{Cj!&%NqVyvP zgQb>ee%BHxUk|bgg%^`or%usK&DGBci3e{KP({x=4TQBH&`V>QS6$#)_7 zXd1BGteJVBdy@5fPn0a}6A8|Gw4u)g0!4009M*?^sfy9_!CzcawlBG9gv|!;E990#OXJ1Z09%MOu*-_dhR46Oc;YsOqr6aAPan1@ zOx?B?bD-K&;yDSe$6DM4GOQe9sms)z{Y`4Ys-M*uWC@+CN z^MuXu@)@S-?Rd$<%cUre4`pTkL}2$YtVj*>y`owbZTYei+jfD+LcUE^faLv;oqr$yQm^Ty0$=?! zYMnK?zy@zDSZP^a3AX{Q<5qkpEEozK_4f{^&ZSnm>wPZwt!iw3e!Cnb{VN^AImx7) zGG)m2nU{iE*zX(9c|urf2K~moY?#blsM^BR6w)Mavk;YO`pek)E7`gYX;-Xz{Nm@v z`aj-2|E_l_z6(Ov6r;$xy4X+(hB8lqB7p|x_XiayuXjFh#>__oiLXSvl(XpJ?7c4l z5Zw%D@{jj30JZpPEv}QQ|MdKPI>$s3P9rJKpvG#Q5j`8R9&V7CLf_3v*a7|fYW9fb zG1%-mtomEVR7T$uk0D4ib8qT<4RE9jdc53zn42)yQ<3#H*xcOw39;MBzpb+^U#B&t zi{YD?)YFaNYmUrKuj9EK;QuXSO`P-{(2~BF4VdC>l;32! z;Spd75Qm&wUQYy*F4_mJ+rPZ}BYYKtO6ZCX1s28uO4>Jn6BZ0GBO}^cTVEF_Rqq#- zrco)L*7^Da4hF*=?FXNj7#Qr~-hqK=L)C+aj75OUr0C!N(Zt-<^Xt{Q@oDL{v%zg| zOp9Rguw|=+xV^{8$l#AT61-c@_t+G7w-e#Or3(|M5f{_iKc>7Es933&3+iFg9 zGfpSoO>z_(p(wtgK?PCK0Y}WoQJoPr+FXIW@$D}fvNYgOSZ2DnjL2&YQ}l13F*E3#Gc^v4aG<_qmvgeT`y5(wNR*njr z3#>6Nk|2H#&_=Kjk8g@=J!9M+_p3)*TbrdebL1Exe=oOQgDm|n?x27|z%^%28P@Oa zmw>~k19$Y54*Z*rU}y7*D(TNtQVxAyQE_$lRBDu2vEv}(&3Klcbj|N$)Y5;KaYc&$ zh{LaX;>*My{hiZ}LL5P-T-8Lc(2@1?eh+y4t}h#r^lwM%e&abIsrQod-WuZ3d*5Hb zzSUkq(Y3E0{4Waj59m70oSMTjK#a#sos!>}q1$dQ+WK%Df&boTZaZQi%htL^Hjzx; zc98tX$=LhYOzomYw>N0QS4A+`MwT{vw<%XXgK#}cGLA^xV`wGgNw?<24hH+FtD{1} z?*KO!Eqb?W>uf6VFF-T*vF6LK8PwF;8p@G_Ks11#j>kEtcH^qdXWexkr&nRdO`cAD zDx---MhUJSU#66V{RH$2+_cjP{*%;OL;hCI{MorzHlzUcAcbUD^g zRy<>9Vy7m>liv3aW+L_UNP1qkx9#+tL;7f7uClRF@9XddG9vzKq^d49e$nq5`EC#J zZUQ30<<3LtUuS&rTDqAIOhK?p1aV|ozjk)^JK*|qzk;ro&V;8ecD*4u-TF_^(W4zh zuz}~`lK1{kgI#M%(sc9m_WxGm7KEmMd!Cg zFyRf|75Ji%#3WIWTg0725Gl>9^5d!s&pBpfp-kh~^*i5@g4tTSuiXOrsi?2Z$#tihX7_t~Q)2 z;SU8Bg3rRpyzN`F)`q)R@2P+Gg?v4L2!>@bo||#BX|@?h4Iaa{PCMph-oXk_Y47n` z`20GYXR-VBg%V*loWr_ud!-6&|AjA>n{E6cF5ymEyrc) zw`+HtYkY#liDH?$twU#Kv#^6roXR1l8!TkA(}R{PzOo?VRsB8#2KIg%(x>XlgLtg~ zZpwQJ{)~}}>BL243szODvmK+88#pa_2BU=3fry0oC07sw{p-8TZ=^@hWMyP!=ny&qu@W%}>&(PyJ4o zAM8%E(`&--;fQYhqEx}o5EHtGw}ulj6g#iO2WB@#Yq>vy=O=!7l85M$E^1Z6x>q%M zZc)HpGz_B6MoaJSmE5-XS+-`4?2sKS%daf#?T7yE>PI+HFnVo{+8fZ(j3M(L4~e3o zT(r*d{Qr-p^A2b8ecO19DjkU3nvGSp#cY+16t(x>C06ZKn_5vt5hG@8jiRkBMXjQO z+AB7(W7S^o^L>xw_m97Mp2&S)=XIW+Gnbsd3lu}~O+Jyyp7oq%el5K{+G@@4Txm0^ z%yY-JsX$1ze%FyLJ?V)kuQ${#ln$YL!t&_G-OUsuP=QE|GK_DPWyBaIs&;-- zhTVu9k`bj4SI%HfX)g)7^Qw8s;M-DSsea6h)ten`0ejs}Y#KQ#HjfPHrgFRYVrNnE z62_N$^FIY0bRi$ADNC}ANly+#K?W9(lgRyyb8&C0qhiHbwt=24)egJ zcj<@Y`XVh7_NJ9q-oEB2_ovHJx)tES2%hA@Ot z-$)*%FL5$0vo&d7!Pk<{%6y^-z|QV9^X=|VpZe`3t_c!CvtjcM8JZv^GL|RH3M6Sk zZ}*>b!N;7p@k+kxSz?}l+LcFHw>mf;l?$m_u>UU9_hdNG|HHQIC_y7rqmD(GDDg#h zo9xIm?ZfDtQ?xx6s+k&69Ed?R9US>s*zM=a-MASc9R zGkG}@Da0k2f=(v1FUZnFDoqnhc81ODd|8LL7IU|hy56T&|GA)k011D@U`z(q+UnPx zmX6QH{WWNmdw?^grBR7N-DrzK-J^lpTqrXpc^_v*hbDBqOkiV@<%Kvp( zNfuUW^|L}}J?+DhTF3Lubj+U@g`1c?LnFEoELRDB)_6Uc1gthJSH%3HkWJ?M;aw_A zv>qZ-np_WL%!iI^CCC-#A&J8gB8ISZmO@hLj7%A0r3ml1=WzAJ#-mKWvH`6imFq7} zs4_)~5u0qc|-5Ith??sT+$fE?o9E&)&8Tz)Xyu|%-Q_B zzTarNdJV#_er@?%$I{6Nzm3^Br48oFmC$(!(kGVTty>mk68SK)xFx$o@v`R;f}@LG za?c>7<6dx9zv!=SRv}7vr2)Nv5Xuccmh@zq`o7ftgBkB}TB2?W&kcK?deWHHl;tjq z7M%Fo^kEPw=QSIF+#h3#OP${LOIub}_ws89y6RCmZ-Bw- zCgZvH(T;37{rlZ=QzVm$C3T3d-lL*W7{{%&?TrR9xF4q(zMl-YgaxhKQ?QiTo{b)6u)64wNyL8@s(rzXTK3#mfI~tI-wIBiwfs_o#kDsSXT7Jx^3<_9Ci1l_c5dSJEC7R7~kpI znQg$fSd~l*+Rz;BEB36bV>91f7C+p*dI{o_NXGmq>4Nha_2zH%7A9G{=f$L4e9n)`t(bEB7tDy>idNAe)_~g}$UNlI zd_)}=bh+gjGzVa>ijYf(6rnisP{2W*l$9e%xEgD}d4Rfz;dLRZ_d8c8_kNhoHygP7 z>4Die%41lFq|pw4f8hoMzSQ6)_dR`*j*aSLY1j(x!|?h#Um^fO1a%wXIbGJH-zgB9 z%`MWNEmFeDA4<1`bt0{0R%cwgu#U8<)cqvz%fFa&5jr>zCz$B9-U^p`{rPV*i2u7Q z21{YNWA#tv`YI1*ohyOD!#HGL}_Zf*3!6|Us!^jm4uw6NN-pyt1srEt^R1O zgRY7clFOlPP9*#b>mRot#^{s-f_D03bsJ|xv<9d1^fS5#3$LQ-){s-dRo+7tJ2CFZ z1ShW9+Pk4chw2$zQhXLU7WJ4`B3(Vba=B7PA+49R=`L`q>GzKDgR~)-_4Ed3gJnZ; z6)Sjj+4I4>?fDMe%jF6&oihO5LWQp{U!uy2GA{QHWHE&dVg4uC`s?PoG%d4^x^v5P ziuO_aH$#C8A3yhI?gvs*I?u4mk)D<>xqohn@cg4DSJJU+pMmzsK50naHN4^e3H?1G zdRkjK`Mwl6p4^2vusZ4{{^!EI96+;4ziUGZ!v3c71)8fJ;bnTyl2r`YM?S-6CDdNz zy7anOwPsRdhhs>e)|Z-{BD5G=V_-N+!;fwirzj|c$4cPx4ka$a}E z4EbOhEt-=SlYUn}YUudJGq;P(5gIV}gBLFMqkVz~Sg3i!y7OQ|PPrLdE^fXPi}u@u z-(wX+aEuC9gPFRz+dH>lxjXH9dwijc7J-b=>$8&JmwFa{D-z%b>N2Ydfy)oU(J7>Z{gH!>-C(mwivFh2mp?82|v~wT9rKmHZZVSfAlgKGrjKL zQtVGHj~-W=*It8D&*Rc&ydt~C-p2#`N9*Sl=Jo|pex185P~w_*Q;uRB|Gl}|5) zgI?=}EGu3eAcuUa?C>p@zE@A6=N-Iqc~N#b!3?iIG$>uQt+p)xUD-g~sQh`fQn7ip z(Q+LQtYbGefL-@-@YPby82i-%J5PS@DJmC^&r7vpO5IFB16D9Zw=2*o4mz=u+a&@| z?Hoh?0dA{`Us#&BCCiF}5o4e>;R|{vXeR)5>KnXj3#yz6BC+hh3zZMtl$DCLI9t&W zDY>jL=*oBKTR&aRW$aN@>_U1Bi^FW2%#o1k?cX>q_i6mNc>wq7#a7ioX=^13S1^m9U7@ICk7|ADSht<;{J{sN>kY(4As)fO zJy^9L#%0{4j;xTG9^Np6P|7NlG&s%#K&)f3^{Hx;-m|==)PpjF>UWhuemtK2Vd?9) z5`Vath+)(zvmN(EH*P+GJ~66`?S!mm{n+v~;~NtfRO90TOah6~Fj!q~J;{8Ea9|E? zlXtLCTiuRq-xdZ)vmDQe+?nxjA~XsqQsPGSVUp9Army>k8qXa^iOHV~JGkmEs`o1e z4U3y^e<|7VGCLkp5uRLb_1>9%3Y~VTlaZRM1P);bC{GY_I=Rxz$K8hlUBx2jUC9!ztk9M&KDu;l5GZ0{WsNf)TE|6qZMz5H*y=*i3bfC;BoLDreauFYksDt8-|8Zhe133OCkv#C>rz6Re^nX?T(~IjKN9vf<)F2vxeD zK+naO@)(p88Jl3>;?m(DLhtFT@ll(M9E0$CA9f?(#nzsoVdRBOXRUnvu6RIsCTA=v z26`K*IfImLrO>U>RK_+~4tme@{LtE-iAlH0cm^Yb0;FC3@AqVFwfWHyt~*lRL^83^ zhG%fAkBPr@N5!RL^X^k5Z;zloUw(3fk5Jl()sk@Ea_&MyX}94^xH+KzxGj} z87yv8+~?*^zju3plri@md`a}8%$NQNf(BtO%C5dj<$hiAh$tw9;hAlD4R8sVSd`~o zUIw1YRbj2uSklL4h&3Ed-lt&LK-Sqk<0OHn3v^@=>o0zPaPbG*EWw1Ts<>=99a>@((Taa^D?vTS>4G>&`PW2@oG61k&rI>H7)Qj z*;j1+UePUVKv|^x8?obrQE|K?idboJ?zwxmyj$_7fn4@EaSx7GP7}wgf`jB-;Y{9m zsM#>ps$$4?Y~Ri5&31z5H37FJTS$-0yD zPCwqh7+<<8{b>qYiT!PX?tFU0FH|5(4?zCy{xaDpyV@0Z#XTyDpn3T3A!QuK4z8Bm z))PGuJrtQ>3fdgs@_=W&&?ml3aJ{*7`u)t%=_7BO-1;xP+4$F93Xgw*kltlCK3bE{whoOk`!8mnhWe2hQ|viIb`Jzc)fmO9~@IUZB`4 zzJ0>Jt;Ta)Dc?IR)se}v%D)V2TA{M1lj}KYsP{}21%;DhI|@>5 zFCK=+Z{Zbpn1f!$s^nhpa9Tu%SHz3>CyH0l!RR(QG#1f-JTG1lu&?>jl%F#)t#4V zGzR=HxBDRZJ*)Pq^Ra)uf^z^6Q#|D&!CMnv!Qp#mNK^h|kqgD)&H@ z`cphHA3x+9M>o$6=(=Z{3a!2p>E;(DTM}TDa+^YH5tnC3W)(I$zNba@&}ezMRw4Kr zeyo4PJT~SMHsg7tGxQvnWW2BZGEEm60O2A=;X8WD)krNL?YDmYU6m8CllO_>ru~5( zpC~~*$#` zoaHebA{ws5(0{*1LFr)@f=$bqJ>E!O(<~bPup7Kb^#GHoG-kalyO;gq^*vTGs+ZjP z0%~h~OMC;0b8B!pXO7jU6F=W$Gau)|suyc7ypKQP&0rSO;4Pq%4B(r-h!ixGh$xz(+UC>5bEf+kW??#>IWjy-_!2%5D^|x z4v8X*#%5JTDP|R0MY}#1h^qyFELoJ*zz>vRUZ1LBLq+hX(L93_3qu36(R{z9J!Osh zj6mL%MR)ipz=*qr@yg5Jed9tL%ZrX0?v=S1^T%qui%Uj1S*2qr+keqYIFtE8Vnxwl z(K<_p7&I{x#ZwSCMLUB>loM>Fj9*gxToiT#Lg&BtiN614t`RDtr%n=Kg#5v&peRPnif=dB8V!bj_{l9}ZvHr)niejmt7Kj3ihcMWNQYH8BTr zTyQgPHdO#G2A}mlchPi7d&#%7Z!nYnA(I@H!b6+aMzU#t)+%iavYnvdPo%GOhtk68 zA3zS!V^u)I9F8h=8Sxih%Qf|fGb--Dg*KVXU#=N!=E^biR4Vq;vR`euaLN^x74xX= zXNiNh%e$Ys;zZVaK{0DoejU&#XgJw-ZssVf=GumdqXe0GzrUc zxJC9}`qyzMYk4_2pK-TARAVv*V|;_!bg6kxLn(+wq{4|wmfnFO_U%G;K3Z-vk*IfN zh)BZ6x3lI?tLm!qOAa-&Ic6`5sO6to_ZFA4paFsC4BL!}_e)mTN$gp3{kg|?$*P$O zXw`P7?PoHX)q=x}?(wo42;y+H7kS}sBG4z_48_rZY7racp;n=ob~F0I;>X|kTMg7y zwqgZjddHj3pVo{4Gf(+uxEVdCnpj?oM|qU24a&7TqD%=1BB%x17?W-?!4(U;}3|hX=TVHHy&S! z@Nfa@A{eQM^SQCUV-}^>!GH8p=U7CwL7%@3iYTM;%vK69Ia?X!A};}7*@BFuvqYP z*xc;_+ZS(KKwmRb5mTKt*N3%pqmRAJ3^u7XcCTe(A9|uG#6AX*r+S(I1_=I0OpNZ&DFTkYfO&!rRQiQ84rF z?Q|NbBDOAN(Gw;Dj+G5O`Gf$j_(t&HgFY!D+3Pk>*grtCFwnd1%027R{eu#FWYe~;;(&Cx^t_)#Yoi_2~hH?_S|qIq|RxdoP1S4)EI*cZB6GDSbl@M-80HlXSiumV}&@0BihWst^DjAT?`=?=9np zV(Nh$^oj?n>SC%y&dBL9I^_DADQaQO{>JCA);kRj$LCFGR0=$C;%O0i8MUt@^w6V2 z1!Dg!fM!FrWRQ&!PU(9)AfOWDVGb&-S2!(UlTdR3DfPtQwX--xF;2;bdJ|#pRN4Bm zcQ`0;K}=)@z$*^}^v8I_26d?Sefb+p!o=jipux9wKKj_0qRa*s65Z91FfMLu{NY(Q zR+m4{53YX;)iZiT>;|?9Llh#@dW*pzHIw9YXHamR-zRr*CsIO8ycx zS5b2nNp*2ru{oyL1y7~f@yR?mxH&kN!4tHhlk2f|d43#ov1#FN;i5Q$wf+wJ2cSR@ zT>5EQAlX1S%6a@YFN_qIj;a#rzst=&O5vVq{U=Mbm`wR_Nz;68esWps zH1clfBV*bWCP03R$kbT~M8^tZ5gwC+`;Ip6WdF0T@v*OZ8*SCn^i$yFfC4PITKY9# zjYBIEN2fr=E~onCJ6fhrK=8Sv1nAY97B98qSbb$8L#ro!AzWV`!;m&B8_hf`D_wyb&$ww?F`R2D$2d zPYTJcD@^ohbmDv^DRzQ*TLx$Yo%u~cJ}+w`Bvw}Ck7Xc@?n@()D&IkuRewDid@KYQRTd}C8qqYI zK7}%??^w`{lV%x4ql~EG?diIg_oeT&Yd+5TL9gMGlYz3)A3Yo#nsLR+a_75L-K2&^ zuMLfodHWyCZlc?RReW`a{V_b;K56AQrNdu4f|;0TO^9TDdYf*LFV7$0s!RM z?C-}tHL$}2e(!l!ji*b59shBRW4V;Ndf(PX`EBsz+X_Uf^R)IK4{SP@!%&}zo!T%? zTJAx?koObc#(?7rjBx5EQxrC^VY&;&?I==&{4pn(*6S;Gz35mo z1X)}k${%rFPYyfzZP&@| z4YusNdgIYv9-BvVXM60|QFd;A$G5M~T8;(-sOnbDsPec3xaJGV-f(K_Nik2o8rvvV zP$wsLf_2eu`#g~rX6!PUa>J}u3dK*!-jVQh{4o;TQfB*I_1tP$@^;j5xbJ!1YZV=} zMd$ZAGyF6XGOISatj3O33?BKjWyeDCscZiRu43noYAllFuRP_?_jj6K^JFC<=*kJ| zwAaE#->SF`%4&G>vpgf;f2Dhw`W{Dv*)ja6+n)cc^+i6xv5AHzVT(on+w1F>sVS3e zAr*6q8svk^UzHbP$j?X5dgL888(lg)DNS4Jrnlvb3y*oJUs0h8&4iS@y)vTS zk)DmD>7Ml!;ww=CQV)EkevtRiZL4qT^xOLvusmNMrn29BPViSmAm%%ew+#4^$V6S| z`_mihQOcYr_5}LqtF5sup)`sK^1IUdLeoEZYDld)YamP+UF@PD*)__s5K|S&(Do7zrp7tD{ zk`UqD6tc8)cv}JWlb3U}4fjcMXv*k6eHq*ROeGRD<~nBl^xnxC-fnTDpZ0F<2!||H zndN=4LESSCYRAJYMKQQ+Ec!-rQIAJM7KQ%-4b$*@C5R{Un>yBKpD*4hTzIW6<&ejs z+=)fnsG~n`e~jp_gZL+ka>Eb5$1|9kMN_p~-fW58oR5wfrQkw{DPd>smF|)b87X^^ zw>%bDD-HeVcZW2TNiMOzYoB?3`DVsykI-1SoCeIQgYG^U3XK*jMO6$#9$M#TeQeD2 z6Xh^(6QFz-4}favX7w2OGIWCzD!i;HjVJ;f5|%4`C}|^ zEDSYz>t2mo8(pU?<8y~UZx9LDk#$wAN(;m zmtyPgjp&``I6xrUVkdgywYdL9`suYp2wmM%7UJmMzG;Q7i!-dp2jvH9+;`zVQlA=z z>xE2+7JJMS`qtaQA0FlyZp$5cz99qKU}+GQOTQlI^zfW7DNY5pLKc^}X`xl1-&OhC zwiVtY%ql6dub{4*1qy8IxIykVK|%fBem6d9QHSvVz739qbD!CKEl~~)=r^Lk7Q)ql zrQK?lQ%$!Gob9!tRz4u?+xIT%-V;M)Ev4WPk8mmETn}w60S=j9S{!O%T=ve(cBE%T zkwU4S*E?u^6fQQ_B>yd}h?q~ogv=J=lvegGWPJNgC!aThiG+ig>YDgah=roDjk~nCjyswZP6%UCOMZAq zG4VoPs7!!%8^l&*2h%z;#q-5anZ^IUUiJvr_dSdxz}z{)h2Z^Wd9-`6+*EP%rQ!PZ zw4Oe8sp1s@=!34Ye6+rkts?@#2L=bS+=vdL10{x92Vp7Uh?fQ8nFqvf_+_^01d+d* z<-cE`6}YrUxM+R>P7THM^e8bt^c$zAciAap8a`@o*`Q@=L?ilSH=JQC;c{MCQ0^A5 zOIdLcEH@@3w%bZcJ1hB3cLxizYb-PUSfZr+gM!t81wFOlcX~PP-ist%&!_4GB*LpQ z8lyF%s~MEFqg$hz>%ZHCmz~P04RR@pgr5r(>;7`o)3~G9FEgTIqx+|O+7@qoESe&g z;Hy_I;)%TH^>sDT?_ouS?Vr+D9&EZOh9vFryC}2B8^7f~X=#dUzq4#hHyq72-4|^goA7;pSmH+F}l6hzQ)JO&#ob;z& zehKl}-7Kf&#m54pFQ&={PHHhxdDJM!zj>NY5}xIf!?}^qkfA|qGF~5kx%Vq`B(<@;Jui0ePFa=SAuKnl9QpGGClzscn>5`a=SW1Sc&i+(s+6;YYW*^Y#JoQOw zqF7ZmQ_~J!$Gm?s3J0k4J+I40gJkCmkzX^uj~ZK5n^p)Pn{7=h^Zl?l{W2m}nOSwm z=^=MXp}$(QAFx^g4&!sqT0`e05g`Zwpo9DP`Q6jeeA{E~>hq56^ba0{>3N5TgO-&9 zvIl*y8g|Y-=MJ*FFv2P=uRI&K%~)M=E!X>YA?ImV3zY8LyZ`e72nX-2%+&@1@O>8> zlRiGqB8L8D5y^<=)QDCgtWp-X2aB=x^CK3}-&}%Q_1+PlMyhARwfGM5Q}kW4a;h61 z%!ws_GZa*()WEC1p#Bucq42gQU5T7S6fZqe=iG}FDq9)dQH@`$_o4!sV) z4Ewd`+=*Jq%G{}xhQ0y&+3&RA`I}cw+x=0-eP81h?3QHs{}O>IjwvJ1Pr$L7*4z%u z_H(YV#*bmoP(Q?2uu6^!UuCN9CP;U%WJ`atB&#||(Bh-{;HL)& zHfH0Uj`fBDY?a{&)4BgraWNdPGiuGWX?SCF`j~8OICzk44u#kvT<8b zAreKbdNi3#d>WO}D{J#*%|$3p;wSXgw)TsaxCE zfF~f13$)pJf2+=AhQmWW-*^$!L(3F)Pg#im7XaQg%|kuCzBp)w49Ec1jE)b|uR1@b zS|rAanjl3E1_!lH_*C*;gb8xMY;xG1gW@zp-E+=##Q}QUfc&+oNDFS}XNKe4G2FXl zjyP+YKJr5wT;$)BQ*XANWqcPllLuF7|L8y*@UQ`M0LabzxIXHn6P&6IdQ$^p0+|Rf ze6B0fR?LOKU4Zt{apy`jW=1{iCKcPaZjtmt-AAwpT3RiL$8iN61hRlzUh#N)6R|cU zQNo4&L9Mz4vP!=fwNY9`=W9~V(2#iO(fM)Ikvpb&y*dnTA zv9lG7Yw1R(56Nfesg1GYAX7hOhoEY=y5!-MfFQT4XMI4;diK`F2Ab|RZ_t68{d!EP zvjI%}SloBc*%wyrTnCT(fTU*r@yZ-zD4>70H{vAT!Ih%J@-G6-lKb_QO^xvvM{b?r zU5Xo8Zz~aqWDvQ~@`_6(`lTRY_OOPSF^#!kFw~j}O&dSP3lgfRK-g=NeUj#v{ehQW zt2SUo#_4pC17Q$q8;) z%T7(OQa0HLy>0Icn5Isneo_57+DOdvp|O8li(%ik{UA-xN_(B|Hqaw46n+>~*)g|! zXd!>6{pRekHzxQxKscnru*vuMZe+PWzv?%-71*6205+rSBJdXPMuNjpfH`e=O8{bR zY;QDV|BC7->HP0FX6(h>b#_AEm56hGaEno8S`7nPns(IV4go=GyC`xgQ5q=d4Y4M! zTQ+V*^-~wLb7h5lf*T4Ip6X!PxHGlmOAyKi8vmN`JTDRdbsJwGb#IM6TN?S*H;OTLoY~-PT(+nXk)~9XPM^S>tgT!tuRIJ7QSONMjl>W%*1av6PiLg*IEDCQ#k@zBc@_W}hAt22beQ}qDf2W?Byiw3 zu+r$VKV%()SmO=Z=VgrT6uxK^7FRn+ObG0l0Xj8bLj#T`eH+j1oTK^y@D$nU!0>A4bBO`@E-b*{l$Wqe3fg+Z9y|NwPCHOFdZ7W>Tz0M(g);zV z$C2ykV_ZwfoGRd2L1{mhldGti#T@H%1VM&`fU1dxsZ{O#(*^|K5`wP!tlj#xn#r&8 zwZA_qzvUQX@zre7+&O)?sg8|C%kR`T=I2N}%7arKa5(4-Zi|&5b1dW)VL-hKu;)Od zi@ebO2l(~9y^W$YxnEd|v!OHq^=RUY-8#e^zp#H&kmZi>(39H60L2e?8%$ChAzsaM zk})@OXmYc25c4MT|I~CQ63Wwh!I70dt0zdK#$?*a@#n14Cc-fXQa`U>{Q>MYJF&Rx z+_Sj3d7NpE1&vlK;*eIWpEHoXZ2!5lD+^t)nW@*Wc3w#z+Zz3s+0a=X#|$`;IEquy zRS{^x6e~h00?~EeyZ5A16#HSB=O|YFPH|M^4iI_xV44w+rD#z z$I<4_bv3C57N`4~jX=7jcSd7ek!)rMqy1-PWmM$Of8`^Mb1rbrbKu!%D-Zl!a)3NG zXYZ_j2K$by3Ip55437O!Ct8P9n+m-N;fIuzbOBOBHsIN1){~ce95yvso(W0Hyb%$t zmL^zkZ>8sC;PWzZG=IcD*!{+@p3cEq!SvYs4RyV*ynCI8sDCydUe3q zc%FPKA+`K&Sid$NvC~M1Qm6&aV_P(_b4M-*z{sOoK?;9$rs1+H?<5gY1(qf4QZGPlgfU? zL-MO51dgh@AnE~Ahg&ZoM_O+hVcA9ZH#=hH*!$1liqaBPz z5zDRr95KUvN3+bwW$W;&dr|4Gn=TmoM5^H3aKe@MPH+N-EjJ5%e&jgJm{OrrH&eUh zvnOG3ATd&%d%D&8bZa5}E;+Qp5v?9~-t%&w`hPMHxnQ9E>T7&an)w+XFrIDuobM%s zT*OJuBCaLww6nB&E3K!SMC9LA#dR-qQ2G$dbboaiD=T2uz{vzfF^@bw>EPzuWjmir z2;7Xm-nY048wlP=yH377sl2>OlN;F9wlR{S{ak~-3;GqOq70Jx-cV*BsYLvst}omX zCx9l#dQY$lp3DYUdfZVD4>yv;IFtPe#u!!xq?5-~J)wW!U~n82nOt9{cGEL5GGfFSt5VYFq+wu(-saGHBNz@!ado4J3wLO&^aypy!t)na)ty!O=C939LoAjRcf>rzgk3zpT)WW&E8)-0 zj}l8*SgG)l6Ps(^V>fx|N0PJ^;jMw$=8lok_mJ#db{%dY=K04_lXOv2c-;2IEBqHM)yJ*X2Q1+b$raBc zMOeP)V(0;Lcn05+$ZK&iorDpjY1GGc$@c`s4|e7|lRIh}}o} z8z8RF{%#u0FW8@6cTKN`ft^`=+eL-M_C)R+j2-R1?#n)~{Lx6Lb1h4>+@Cup*;@;& zTmuWvPgu{%U%Svfee+tZl0b7$B{ZXIx|=em0EU;5{g5%f!l3ud&iG~hZEG=hPRfNv z_A%Ku?qZd=!+6xps1>Dmbf&u4k!WZWpoIIQ@+nu7{Jx&Me*y1^@;@o)>|hS5*f~dP z_iy8;;=z&Y!ZJe9rrP}#sFDE-?Xe6|KVyg3xl!(l{w92yT%xsd?viH zlSJ8ezW0l1$s(lTPQFb}$&Y`5Qux<=wa%>)v5oymTfqv#uJLUE-5kM$zZ)x(^rRuR z;wJB=)8i)$2g|8R|AJZ^P58s98^9*cMCrnw`lHU<*$yeRC0G|ailLMY9t-btWWBHi z1^dxK_(nF17yf&om@Dz>`i+}a;(-4n2DJLwsNPfX70tjsNjRQmvCXN5Uq*3gw3Ufv zoDtMd+F_=aN3HgpXCAg30keUre&T8vnax(#3L}0qX28dNa>ggPdLW{f$o0k!1QK8< zvtm+JOcboD*BKU=hb*EgSB(XKbXn24noI+rzPYY?JO^p;@ z_0sXLB}eqmR2BMV-ujW{DG^6Wq7=50Rpz;;tmLAK1lGW zJ-Yb^4$Qn7nQcSXQ_JCAzwaR*KB&4OY@0+=LaViKoOX6xc$S8wxam5OcE|t7wk_%0c$(6UTLU?QgY4q zkFhW%><2V@+PVqCAQCAQ%{6ZP`ot|04CSR=FOcy|mm?|pxPD&z#P z#-Fzp9RZQ})g2)BYc727dX?&0{5{2APMNkAR=U%OtYnbeTj6H6ow)w}^}gxe=(4?m|C|&>jI99DJO&P??d+(6p4W5CPBjJ2s!aBW7bYC_H1<%us} z%1G1{9GX`$TejuToaPQr2Y`w4i1&Kq@0X+>h2ZViMnQkk?*Fnh4EK<$SGy^fHc>M? z*j7BrVc-zehVxseYr``;78|^d_S*RB-?9J1wB+d|H%Z>m8pA!xXB64NyLz~!p(-l0pc|6CJFJ{YBAAm8neE<7>JAL}du*TDrs;}c+ zYQr6M82@(_XtQx=$xP-t7a(oq(QibPZXU2L5$#fE0s)Ns%6!=XMXsc`ZKyNy z_nij1;t5$nHaMFwWM$_xY{CPw2~26@wSp9m2@QrciiGcv7DcK+3B+j)ew$>hy5#sP z+8Wv(&h$)Az#>C!R5{*(HGQ^R;nPc6r}3!)W4v<9H{otuzL~UmU6v(TIdSMA zt*^>Gh*Afvdh#u(qh2U7rqtmT4Eu1m&)BKT-m!cS$ej|67(126R)eVHM-_Vueu?U( zumn2a?C_+~HeCY76p-zbTmDTX4p%z$KwK3T&UaHK+hJ{VzE4E=+1*>SE_LwK>BxBh zFFn+izyg6Hl=?OltLP#BykPmXG>m3(Sru7QvGT^O>jiRA?kvu9;Ehf6$?4jI0FmA= z<059aM2-7paHR+(sf;9nwAd^tGveX5k(m{Gu-UdR=*-w!h*ibwGYtUYtMjzn0TwFip~8244s2Y(TE7_Dn|ozk>wwMw4N6FjH5{e>Y)?H8zhIgl$<`e^LnL60r;; z{gEh6qm+>*K9G|`KyUa5B?Jsi2COD{?u<*kXp0F?+g_0i!J@xZoNdVU%LL>$g;1rl zv$LB#WU&)-NS86@IbQd=|Kbym5W{wO09DyWLzRiQnQQKhcvai<6W=3Ka;t!VOcZ;- zmMElgB{dVatmf)Z8yX%?0u7j+p4AT(F&ht>_6>AWrf7Q*k$d$eM032Lq;l&wQ(bMX zZi*p@SAB9{ljOyBZ9~GlUM8(1^%se@6jgpIu$Kacn!vA4YQ~4RA$cO?W|P=YqpH1sNS^ZV3vG4=8L|ik8?7x9rB$0IaNi&E z-gDkC)FO80>nX3OK=M4b?;u{a^^p8dd?OEQ)`c~Y2K(J^^ z^;FMBqmNa_W(1M{3hawhey_N3s*Xtgz{dQ-hdPWCt82gSNxJu1^!Mb}a%ISY)WGV& ze}atO=;G%C;HAF=%>vaWsSm%R?Ia~}m&KVbG>HK_P2)5FWjelW076m=$G+=ns;f{y z0MVdE$?&ubrX3r+DK&Rk4^jfPlf(d-h7y#Tsk5!^D?Rz$FskLO6_x91RM1I# z%ba+hNz0CFYO^-IWOUxKNg9?*$wue=RFT&GS0ujnoI=m}SuEqj~Tz`aGFs#|gl zSL18~fTClW-VA1&G-I?W)cCx&f8LKHs?H!Xot9U1TsnlB9r5NiKLowUXefB$~OVIcIM-y-Dt zJZ24OW}$pw=mVdfmE9!hD}owS;KAQ~zh<~#+{_VUQu-vrW54g!RX6#OrS`jIx%VDW zgXB@_eRnt>x?sL>YHz;<@N^ry?;g?A4rCjouFf8`Enh9`3~>DoluM&VD`)K47r9`! z*4B*sdp^!IRg~=`|GY3iqeU()Zfry#bZJA|b_9Z_xHX)n8Fp)(W0a(6&fke^kA`2G zL1StD;MV41p`MnD`6f1S7hU_B-V`O0W7R6!a|ka4{lWpyXIT{4#6i4{j^G%b>~pr3 zpO}^!9{Q*Dnh3ip2xc{VVq8I(4Ut&MI;e31@F9CQ*&aZOLA&8pPl?3r&?e$d_xZv81kYRmyX`BKJ~@U`(-U<3u6GCS<1ti>?@qs&!&LqUgPa!>(?=QDS-j2j1Gi(FvgUm)Ic#pV0yB4Q zP(UfoxCHcPLy|lQ9%$YIp^{faJO~65CtRc}Ee;e4Et^MwK=}A<7Ppy6q{d-LM>?ldU8cx&zPnm`U^4_S?If?J9^Ex=Y2-&`&P!1Ke=h zR3oEu%iP&hPF61T6nM}N>TrR1Te`(hc}>!1f+RDGkj2Aa9E~k3?C|RggMp$?CtBct z0dEc(JyDl5O&AR_aHEF4(IdLGrJDSNp6}@WnHZGQ@^NL*t82nV$%AGWZ8y|*rSYG znN?4_I)r_Y7@4MxQjY{ZGeZ^Ao3Ca62T?(;z8GV0KRPi8fWIzEh-6<(I|xQ z+&eDC0Ek>EFtkA%z zTH8Ep?oT{*Nr&#>cr>CQIVUsgQUWj;k40oDY;yWU^I42h4KUMsZhNv@P5@RLreu@@#1_uO=%kG>T}llvUY04Ei@jY9&q}`G- zLIbGxkDoB5&PfB*2E#$G93@J?9goLLK=c34QKsB%HY>ogrRwGq-*aQ!82^cmkaIRS zB`{HNN^YK2m*f(rVWlxS0td6tO!B-}Fz01O=ramKr@pw3M=P=fr4kZDWv2`Z2v8G*&!s}R&l2nod`#$iqj@}9N=Xem{@yLhQvkq8Y~F(29(RIsi* za1MrV5`dJmNbxhiu6F@Ivmp?iT!=jw0Wc%5_h#eHGPLxKtS}7-reJZ;Lcx(4AqJRv zATu*$;t(RS1PMafO}wBrWq6w?cuZ3eIYEdV&H$MuD)9MI3l<(;F_`;%bQIeXcSlt< zJ^Dk<;V=T!k&7XDfWzI*$yuOGJqp`VEJfK}iA!pPQPYb%Gb3k3PEAwg+)*zV1|nya z1+2`mRHBs#T=Xakh|+UbA_}o@4iQn;buo^X*n}m>lm_e`hOkj%@7XUQ`X5v7m#rQpgh{d>cq>Q92CNh+|YFS)DfU280GkYP> zfX!@40$X0NsyaCliKD;7e#?3`g#WZ13$7z)mmjq}+!7J!Got?KWX0X#WaNDd)7{Or zn5LVVlSX0?iG!`bh0_^r-vH}BOUamo764HI03ZNKL_t(EJ2O#5RJ873@Fg-7L$%#{ zxo`!*f+d8clx7qt#6FM-OI6F##lZ*M`susIV7xS$OjLXKky0})BZTN~Dd(aUzQnd* zfLCi@nzTsfxQ>*+K6wh0&w6!{;R%pPBmpy+MJNtVGfX61*Hs*3z*$$LI(J@xE!`p& z?~1O=OU7RLr{Y>s{xl6t=i;4X*;T!X(bS2gSW}3^(&sjJwOolL^<~CcW`Zd~fjB`4 zOb{+503y-MvSkjkSagL9hXFeRU=*7r-GPi99*DXgk?kP_ zH!7tx@39D6W>-~J>bcD=3(wjG2`5u`4OIkGV#uj2PuRThX<8zA)?-oAl72Yl)KkPX z^{d+kx{OFQ%p!9)?cbNGT0M#Y%yNtZwkk#uq2k~|wu}Wa#_UF+x7;WZWUwo6fUck{q(LNaUFWTMBjZiGa$eC2(q{Zy#GxB662_Pe?C3L2wmK;ssOcdY% z3q|z=kY*E(#g@a}Rr690mIq#MpA9=_C+A?)8||n;b4g%J2Fntwl$4oFAwkTEAf37d zQT3{l&dj>5h;TY_%BiZV`FyT$3ex5@s%i&0!HK~}2%wU98)?}W#o5`TQVvupWj9`E z*EEeYsXBz2gPNBWnl;sRJ!==_$T_DWy~a5uoUxV%zNa`oN|r>rIe*tG<;-X3E)mT(yxWSUep#Aqp1bzL$dFlE!Cfo`Rl z7Q#GrEP$G(EM%}g`*PEgmNN&{L|`)ohY+Mt8%0RfA~9f#`7{Owp>wOEq|}$$)fGcV%-m z7mj3LW^(vIw_;{Xxm!YqOlyMM9S}#@axx6r^&4Gr7AG6O)5O6&6WD3>Y@XY(~T71(7B1{B&el z&d(d;#<($VjDNSWJPf+~{{H@Z-t!(2dHw5Of6Fbmyx|RRFthi(=RLc-yCqLsqFVpx zDC)vW1ZN*QD1ez|^k;2A=M86uo8_E5<@}G{{?B%H&j0sc_vNtmr7yUviC|4GV|d+m zts$K|xAl2z@1nbixUu$WFSU%hhW_{5Z?ilES0 z7f8U-G4vR0UInnWQq$pd3JpK{cYpFZw{3pWOKwBeyyB%V{^$dbD$`=o%4ltFOu~2F zaob(D-+I^0JGnji_(S(U@bH7j2XiLRoiK+^+c+A_%Id|PogM7!@xdn^eMS}LvbwQx z{^na^C0DLo-rKusX3u@jb6)bI7jJKGU%q_#V;}v<)h9ol^BfeEYo$>}b@BG|ulnLY zl`#u(<*paKWOlO1JfAJv-6v*;ZP(E_ldi3<-*xB3=U%+C8ISh%4<32=lTSbP@O*m6 zTscCfqQ=&zf&koeQ4UMlOu}t9U3mEm@4n-XJFd0*(T{%Qp+EdkZl@H+*+}(zp~LC= z-1uB`#;O0Y<`SYsz zC>}TytvQ6ZHaFb8X)3@@7AHi$SS*-vJTnrEX-q`()H+cza|slpKvGg-ZoAIi4^K`6 zn9X{UcoaEzc1u;O5K_{dvJ*z55e2D6l^dnm2_SA7V?7yqs(Rtg3V_qs_B-SFXY_q>|8L*YinzOEfxzAxq9^~GZ4sJw=bM? z*RJbS{rvg!=K0{@z}@%uu0q(%Lg3ZO#2MACx2WU8AG+UJAc4S@aCygt9|D4VR?U;9 z>e~6n`np-(JafifXD3tY`xCG(6?rj(R-*<$HEPU^%*`yRfrSVxd^9~VvtnUdk4Igb z*Vi_)8;C$KXqU6+9Gm*-D_0%7y}g}My6djHh-hW9ri6ptYcBlQV~-K@d_D)9O<{I^ zYn_PpcCT6Lyp#E?t!_PEj~b|jx=wB9?ldCT;=Ib)T$3>ekZ1D{B*amM$Eq=sdE1fN z{^21r&(lIo4yPw(d1LF$yzAD^oGqR-Ia${|g`$lRYY|A0n{VDaU9V(j=83zH8VMof z)SBfNyx^{s+KU%261%&T=980|s%~v<<(z{fn@*?4fC+mL9#4XZ98M!q6{-rrnXQ}L z{qajriEyX_5$#>Ol5=^e0H`T5o8_KpMbx%!sJQEL=}hQC zSI3hy_yo*mGXPhwUJW5gU4JPqvghvi-g_?* zmGbUK?!P~!WYFX35eHJT!|4%<%n{k@>M9YjamCfi$=uzc8bn4_MT1Lrmoqb$*2vv= z-(4`g0DkMYev6qIq^fm2a`%)hR>H9YuxMuhCiN)BDAM~p4SILJ=sFRZE#?5?Xk6+w zrHspjzyb%GDCOKG?Q}dESx#ZR0yQy#X**96X>D_p$SG7g=cFKDI&Wi_#RabOHWBf3 zwjiR%pL|?Z-~Ze1C!*QWvAb_=ol7ab=GA{jRY#*q^6c(eb$xw3=lnxIbPo~zm#=$$ zUE{*dH)kBi9O)oVm__UzdhrHC(GzI?gBctZ$9$}Gm{N*p4Q#8`v@s~(PyX3=ZNxzby+Kkit+bzY=3yUtcf2Q1kg5NGY{djDU{EMfu~9WmJ)iHn`{w4BW^*TzSl1&r8ckMJZFB2<2-VR^+cdn` z+k<%!$vHP6^jGCVJz%0NzkU_xoMNn*Wj>!bO|w|ch|R5Aw6m3!mA-2TE1ZKEV^ziV zwKH{HKm5?AVkLj}%f7sHGRiWnn=vy-4!`oNzY1V=ZLKiy-2FFx9ijA`yPjA4W{BwVryd9FwmWZAO&4yt z5QIVqktye#Qku<9%_qHdfUY=&zU|cF`nb$5}bm$|GeF|h;2@y@(84-n?L?rEX zA%tIi=Q~0OA%w{CiI084-H#8C0G^DR$h^L~Mnt=ppQ$9y7gLyCym--E&!5?-#J10F zvQQ-<##l)pSZWup`qbl32xwz%m55d+V`g3~7J-5;T2d$Bl=IfwgsX6Ju#X`B^4H&; z9lMvFUcfxf)%=AodP#CSckUb!J^aZBGW6*uo@7GT&Y1z%)Ga~?(7c#Ui70HWtJ-)x z?nU{SuI}!Mh`9pTSYHRgR6DSLaBRq$oIG!CtP;^8W$n_+bPnL!!AV`$)9KvIR##W4 z_i2}|97`F=q2wudeXFL;G$7Mcmq8T9ktsZL^|C=z6*&llsyehHM0W1%mYF_!d{V_` zcW)7SkDXD9OfXGy?l{J{OTLDUob#@0io^(Gg30@ZpC-2=nFcgC5Ij!u%MaZ}IR#i$C&GvLI^ z!tS}MgIO-^=cF9~0g3R{-K)$zZRbSfNttDBeFD;0LBwV!*J6w>e!=tHeShz;Kn7yu z+#VDm(_Be}gxK9Db<+bW*x4b%BI3y7@z{VXmoIY& z8)w!E|1#&?&1WSu6p`6%7ATYk@M1ArEO5si7ptn;-Mv;-70BDRedg*ONyuHwrcXWf zl)FFk%rhZ`qv@Q8j*gDHuIsu^RX5kxDTbBxO;f00b~5v1$y15M?P8&3$zYD{3peGQ zpL*s>&iV8At`X6wnRGdI-6H4w#1l^t(eA-DA~`rbH0EcXc_zD^KX+3!6KQhJi?;1j zS{W$y1?Tasgz}GkePjG*HT<)}X+C%1@;}u03~bGE^o{C2aw15G|DErACo_M|*L=;Lci#DiH@qQ)@XNpa%a<-)Drf3tUGDTChmZl> zF_SZZ;Fie*;6R<{5CQ{`6m>?qjiFws64#lk`yY6GkwO#V`f7}N;1p7J3M+s1-q(G@ zU;Vl}Z`p1@4$OfF)H=WRsgM8I_rCdGf8?XP@q$t?V=1gv%ALbJW+ReN6N@wRC_A%b zwn$(8=l|Qk{0(2V=KKHVJ@>ukEkB%$4Nwl+lGo!`zUHgn`sQz3)4fOD{~Le(fBAu$ zsbFL}2e^R=P@6I{3k*o9Q|4H)Rp?0P2Um|DynOi5myE}Jc<$`k6q`9!9-&lSyywMl ze)D(zsk?UsQ0+t<_$wz5ISqzy8`ucX0XX zt8&vVuYd8&p8EK$KmYcB((1TPR@HU31o9BVA|;VnY6b8_M9!46!Mv$zq^z1yh2-EM zDX9=w1aVi@P}Mni1d9xJ@4CEl=Ioj6bDL|cm3W5dY>luPvKO?zs%n&~s3n6vgE+*^ z)iW2)Z9nqKKXf=43nOK5_YoVZQ56@9MMVff7Dm~<5q8UVV*|P|{yh%{*|N6CERlGL zw8AMm1rhO}w%pSZ%CDo~Qj6=uvsBmJpG5@o&obcd1xxPU)5>^|jwP3+7P-57&Sb8Z zToVfdU)P{_>ZP%A?n))76h^zQ%jV3IK|(=7)oFHO8>IP5AusZEEv8fyZU=}c5i$36 z3xgl2i%3dsIT)1M!4Qy^6pC_KDwwrW^s(Z0TM9sCW|qsrsSl~8N>x~Wh46u*+L1)0 z{H_G*x-Nv?Cu2!Z5ut(i&=;5n*TiBn*2^H4pD6$|A}Z04h?bw<*Z4w+%P_#_s;tE&51ttXqsp?P?j!Yq?lu{ljJ3LHyHiZ|T z6lP*34sMj(GCUKMiA6G*!m_J5KG8dDIL=bC`Btz=7&2T5B0$rzB zmsCB35MwNO+Hx7id8nw60qEzm!25Q{AKCbZv8*g#-<5{3{HM?Oy0A}{J7i&9lv=Hm zoi13HO~8w414aA5^os{_@e{A4Mrp62tGovpam z_H}SR)f_sdO+RR(n+`6}%UzcS`;)Hg`i_+9faQl-xzk=w4THf?xr^fK5TrjPm?bk~ zDSJ`2ULfl_hSWT{JA2V<Erie-vWk+{sBjH$uIwf@{H)0Ag#=_A_DGlBjoO1T1C6$s;Z09L(#Rz5jxDt$A zsdNaI^XgQ8lzW!6_5fs|>*|FLT~0_0j(>$i0HEt~LBXqP2vv+xwU1xr;6y~O1@Ou- zgk?zbL09nf)Ll;UA!6nw`Dyu+K0q1tlvm;M$CkVL>G|)ymzC$7g+ofIinY5Fxa&G5 zY3fneCU=e;tur`_h^o0Ub7*sNr$tT-5fL|n_sIrRTrSV$A}SYIRaJ?4y){b#|K>cI zOp0HWyO)R~y<5_fFp4r~%Jjz2M#8}8;$dSi0kOyswQU;$7ke-Vi$teFFYEhKBBI%w zrV$ZOd8L`8`69-+l(KR6(%LD{wiw3PU`|u$?F!Fsom!{anVg~S8NSTKjEZA7h=e(m z=m|g&VvHhTFnbe`Sm5nB7XyzT5uZ|GM-!{Gn1{ejrnz-fhHLKNIRHGwk=}hFkchAV zWMJx(J|Y)}@M%GSQ=6m(@}lz0rcpSjHoybJbBg4(5IH1VpY~>^Fre^JgIN)!qjIMu z5Vu`QZXjY2>lLC3L5gA>fTAbqO%=O#9*F~-G>Z_au^{xUP42qI@{bn#mYy#~O8^kc z_sec}I(85dBlMdmh4N;liYX~`kWi^1Cks-Q24r2=AuwPPo;8R0)WE>?B%tfMRP1LY z#8?Lw5rLQ7yG*v;>?CjuA*lM25=10A7!8(CR>hGiIkCab$=wTywqN6foQ+~o6s@z7 zyUSqX=itJvarflLp>iT8Zga}&#LN;MoIza%NlfM>fkW(^C{zjT6hk#`Q+A4>s-2?5 z$`A}>2qd*3WMg$P1c{9qf>h?zj3!w<#M+!BR7RM$9hfZ8ET;?*2WRHku*8%tXEPQe zfwLP88mdd179PCNoX84kc(GWNoUOPYiAYJo16L)gmY{t76Rc2PdPB$M9&1bhl3NJD z;1WZdIwz2@y2ZM3LgC|<`6Gc;v#`VSGESwG+%*JVw6hS|T?-LJgoFTdW|X4=Gn*%{ zaR9j*a!M^=(3DbJ$C$O9jB9rlH&{M(UK>{C&^`x1=$o?3^oIxD#xj%9!PthRR}XgM zLj0{Ic*@KiNmNz1>Q6%o(_;7|LP02$tX@?K#^i$QmN|W_bY+I7RzQ_-H(v<63W0&Z zOzxFb<#FX~s?HqDs24MIa)YARg-@E7Sx~WiSte{HC5SO9p-Y;Rb>^M9J1GsENEFKz zkPxw?tX<09O9G2X$Yua3adH?-a!9CH1VeYEJi{Vg;_iipQqrf-mW|#RH^zS&!#~IP zTtELGTNCI%hB5dK|4BAYm!-MVat1J+PVc+#zIVLi9hWX$+S%E;_uhM7``Xv;?Ckt8 zT3%SbucbF~QRe`R#N7L%p=k&~m{qk2agn;rabNJ0)Evnd;>6Bs*})SV2PI|8X&@-C~t?}PV0 za{y3$(TnfCX=i;b>TPDKl3s57{+cE(~P*By%ih5=RtGV-F0 zfi<<0mDQ6r0YFYduEq=Fj+(n){P{oiQ$PB$dv-iI+?aV`(xO@2xaGh3Yyacle9Pb1 z7@KzoFy+v=Gz$fw47Jn&lu#vVG&Tyn=G9;Lec%0-7v6m_5{{3LL##T0s`1NS^@ZR0 zrf+K0#z8JzxNygFpHo*zrV&8O=l}po+cwWq&K&>Rw|wWX^N@mtHV>gS;-J-4QI+K`GooW?C zyICa^#8YZBTtfKJ{U86qANt`x_}KkyT8?kbK{$pgcBUz%7qnQ#u}mD52&J|kcdk(rh3HhPF5)b%I`ISjzOodOuqh=|7H5fQCT zR+zbz`%7*bqoibA*Et!398`*Zi*Dxb-6Dy|>SztXwY>vXUt3!PsIIvejtAJt%Zbz= zB8DPy001FW%tmH0)~AnfUT~sGIMW! z=%sT&%z24xgNy%;#-6>6Kc*>gsCo zFu-v5F>?{w+}z4JKk&c)l$ixb9lIt6g%%$c0?^kigp;Gj*q79+B5>fY&H|2LG9X0%?PkZAA8K92L}gLRh2^vh05KNgNSMlL|)g-jJh6$ zz_aO7Ip>q(nR%X0=S05f5&(j*#4al{k5^VxN{2_YlhJx&Cnr3ZnRJa;a z2Qyn;od75|hP@l;z;M zy}b=!dutQGcsy>~xvI9^yzc$PqC0hM?_)v2o10rU#FhD?>$=XJQcCT$BLL&o6=v41 zOGT;QPC<@EDvnyTKb@K#ZrCBKz8C7NHO2TAyJk%OoDgy#;bczYz`X0yd~tku)Mahw zi=^4IW_2pkW<(A~5MnORt?Rm+v${Gt6Zb_SB81p?N1S2^r_8DQWCDO$A~!Bls_w34 z!%JBfi-(^t@`EMsR3<7aIt7wnYV^eydVhbH!Ru?Q%s4qYkq`@}eleYOT{oM}a?WSZ zo<-5a$$37X=bXzPScJ1mN%v$ncag9%i7}3w74%x|R29g4Yyg}b9Cuy!U%l>YRdw&` zE)hNa@PlS{baHaKCeB2qwcRb|BH}2uHZwD5=VnZzSz9*%tE;Oq#xrNmF!S2l8WBD7 z^d%zt$VWZ`0H!@vhrF0_Z*Fdsu89QV1w`cLopT|Cr=Na0gmC`CIY?~V)}5}@m1b{K zTXt;CIaD?El`bbsa1%HI5+xa6mUBKiJW|!^(V@G?S`5}LS|=0Y7)Eg@sZ-xeiw}{ABe#KXqS;_d{ z|AF6g_pPlhX5PEH8$#%E5|L5e6e0DK#dPoLeqo;w(c$5t47Qj)HP^bX-F0nc6~M~M7{HY)S5))W!)rA3?aEww=9yhp zefp^aF*D}CBn%Ixc*eB=03ZNKL_t&`n!)Y5gr*e?Q3?E9I`&E+hA^}Bak*E@az`;b zB4K73jYiB~OK_;EndJ-xTgAf4)|o<2PEM*SY;A3Yz}oJc<+aH=Gnz0iIO1f?I$b3eCOEZ+9Ex3z8izW2S4neVvc4kEhy?z=+>D=RBa z)BMse{gQ~h{q1je_rkvVp6~e{5!u<<0kFHfYi4_U?-r47`?ham<~P6j%{k{E`>`KW z&5u9+I5Ynr|M(xq7zmbhbn(vHnx?t`{`(j6ljDO!cdxe2I`rh=5S(+{#Y#5UH$=pp zJehTxQaYYaiO7^g2*=%th*VrUH^R(heJcn<*Q%nvJJMy{IlsfqTW8KxOzcp0+zylAs`{w$FyHD2FFvwL|>d30e>Z;Vu(dB2H zgsV7?_2%ZL6JLGiioqX${Lv6X+qNQ7*LC0jQ;l^^L@6Z!&aADKXSs=P+}Siiis1p+xPY*_4QO&TbOX*7hb5EfzB(n$PEp#X|Kw zGxqlPMPz$x3&6E&hfaM1Z+(58(D!4`Z&e|(xy4#M#@~Ohm37?3Vn>%s%zVW2&}TEMlxD)hbtY zH|sK}(v%zzf9hc~E3QA~Egv5r1BjIr1H}-+>SW@sMkqc}KrwXp*=!a~sWz5sS6$at97!X0-+i};JnwnW1F*flZDvP@hX9hBW}VGuW;Pv9i!eY4;mr0{ z&fTrI-r8#V_$NOpBDddpr-+>0K4)gX@Qd#-GtF3NQad5?k&Id3?9414ON)>hwM4+8 zQ~)gZ+Ji*2v3>?X>MW;}QVt=EN2|;nB$!!Kjr)_6fk@0aEm|Vk-rh0A?$v8;n@&z< zIp@WqE7?(uV!5ZJnCY#z-x^~akH;LS)L)1Q@JAngH0SmuFZ(=Z{`99m?P}x6s^zX3 zH)V&fs%kP>EeBQAUJ)xu?Ap||3DCM$5`O5RN0R1;ANmwCOI=k}wY$5!OzH|NsO&K* zo2p*AbV&*BwCrw{b=pfZ8WE8uW$C!J#vy$hNE7#NlS13C_BWC-%$ zAAIZ&{@~HuUUhSG)16=ViWj}}9q*2|l2W94eE#-3zUUQq3tX>Wd*8qMy_C#_iX3{d zaw6_PtyAZ&KqxPzTg}UU=7YD;Rk3=p&zq#FoS6t#^IvcfEOM1<__7 z{Wrh!z2E!RD~H`wR{!)@{)O-O_HVekT7T77-TRdv`p_@l_uhrEJA!#2fa;bbSG8s8 zoI43FfgI1f{TzJp?qB@Be)uQ;LDELU*@H-7zJ{OU7t@~T(=nHSyuFFx|%laD^| z`+xDj`?{CD?DKx=hyFM6qet%ly>I{a|Mg^^7M7giLPr}n-TJc6d+91Led^=?^LKpn z$3F2`E31bzzUPI{|8rmRl3)AP_Z{&#A-b^u#Huimxg#583nudN7d&q(&VT;be(CV& z(3=&p^x+SG_!TdE;cYMa;xixoO%Q1`jwgrv zm9T=^E)xqGR;y;#=IoU>li(0e4RvO1GOe3&>)bii?dWJ)&)X;hb`a*X#8vEa0@Lbv z#Zz9ii_ywtaokdf1{+OQ@4Vv{o6k8!OP!^*s_RLJZPz98x}HR-JuhHcKq3*sSv`X) ze0GVj8{@|KlNy0~Epm8oJHV%<)Jte25DC~_A#O^iNQlc=`>f*Qvsx*i6XgxS%mLSA zL>fexNzGyi4qX-~d5|Q=0CnW07n9|>9wqJE-3%yd;}VIemO2-@q&t-0a20pwIsz~h z6DyE;6RL6!BMS8w4T9)XBrfGoQg;lcy$T^obMh}Q{c87r-I-JCy zN+DD^v)Qccj9eV*N-hKzc*x0O6oLeiB3o2=3GU?9Q*#KB(1SY5?Uz3J=~=o|c`Ely z?69z;2NeQCd8+046xdxk0hT8eq$;0QUgeS)Td>c?i=uejgwR`<_@HaC1e}&i3ZF6_ zSbniIUmc3Gl2US`0u3qfz)~1(nlT9bdwAk+{-?0 zuZhIPTle&SN4?&?4{zgHG7y)&PgPcqJRyK8Z5AsEfs4PTW(_I*U1Vy3EW*k!2sgu#KaL~KmeJMCv`WjM9h($m~z{@zm%$ap_`P4Q~JcEZCTf~ zb}8rFG|iG=QEXHUUMeQ#NtSX|N{OZC(v?ks2IO)vlI4MEw_Ldc5G*bqY~RVFQ_GyM zgPPG}&%KWq#XFtcPg#BR>;%=qFIj$aDL>`xAyC0kmyboi0g~x;M$0hmGc8>rsaH4t zjOcu7ZWQ2CDUQ1#g+TqL=egV~Q(+5H!Nt21oQu&25Q-O+s;d@X0m<3my`xHGNoChk z3m}fMpaDws3dLW+J%o@nEwB0X-~n*JgN>7jsHWm_zg)sgcA%7T?*@^ z(_iKZ54@12=6-p(ZQF{(GWRj)+AmvWrLR*u3TC!gEJ~4`Sb)N7RCjYH)kM_Ozm`wF zT#u@blB&6?K-<551|i3?q1UJWiF9BM5!#6&69~FXH&60 zmkXi1k7b%?)_cG#mw5>abV}lHnf3@*=BYTtsd`~xn|qfu#;C>;LWm=GPyY{l?;35} zb(9BwRW;{Yd+&4Zy?Q_VK$4Ax!8Wn+6O3_q*lq~K*ft+mcN z=UxjljGK()PHF71?>=jZ`7==16(nchyOq`l5zJ+?c5&IfJ-ycS(!t z?nur=+$fm1DQ1oYA&`;?aWRq9tiTLzs#Q>OMDB_CDml1|6gM9VmQ=VEn`zYjqI&KQ zNR~#uLKV&i2oE-MY!)=czpH!bLy44nMsjznsJyQ;-5opxqUR9oU7V2eG#ya+ZIW0tjGtq_hB_8VWX>kw`Lw8LPTT%j{ao-QfvpNfxzM;kK5JxoO)3aGO`E zu~LLNfyKQr4>^kf0FIQf2-X|S9XT(UGZBe2Zb2`d!40`Hx9-B!xDiV>R8wQ-u2TR_ zvvPMvV-AB)7FRpgxSIC65bxkmH?vx;)+op~EwKZy%mc`2vb)HAGKxaLVvk@Nj4p8m z<)!t7m`F_t4NHZyd4*?Ubr**cuDo7(eVW%l$2RTkUrvih_>W;E{~TWr4{TtN3FLp4 zTxX?}uwCPyp}tZ|XU?3t|Ni^G>6^ak=9_Q6_10To`qGzv-~%6c`XCAbf{BNhiUByd zSEIyj;!H5hzIDx4-TKv=nzpaG{B z)SVgDuiyKwcfa=4Khj|J?ced*cir_{E0?nsdu{vmFMZiHTeR|>2OjwNBcD4Vbc9Gi zN^SyyIk_>H%4!kC^0qkV)zNjQUD-VUYhLgpfBSE~;%jb7*lfP=*dIOk87<7xe*H^c z@zu}0Dr4`72k-y!ANwCKtm{Tsh0AaJ`r8Tezx_{dJ+{2+j<>w=z3+X`*^bny)+#ZG zgzB|uuY&4OVj;lR_uuulpZ>{zzFTBt+kGSd?&p5xRp0WOSKhkB>ge;I{ftK*`bK`?XlgCRF<;ZzUul_0lYhR^2yIKc}w1K{rJcJ=(7(#AR=qZ zZjziSpg;t71u?luvpBQqpZ~RAi+<sYUvbB1}-G^Mk@{Khxkc+Iu%{_VTJ>$|@DnYVmZUgT1{`#*f&d)|5X?(S|vQ_R$` zTyA~$>t6rzmw&_6+bdXq?%b&lzVE&FeeivT-i@pAi@)KU-twk5z5mXi`S6E6B-_WH z^W5kCz?1NAp!~>hmq{X4;!7)hy<`PuE4`xE(xNpaq5(aC#$dfyp$jvOJMb5e3Y*Y&oMX0Ct{mzBmuJpIwdh2G>@9ylaOJ9n)`}s4w z1AHl}Sdy&Q>t$|B(JYBU>iuT3QLSb}A#O9{+z6%hdYyA#HLGx-n%OcpW9JDBxA9=i z1af9_7j7aepF#A6FMPpW&z(7|R&3ymI#5`6?u;7)6~mj!?_*{OG9lLEgdFv&!}k}* zv(;(^pzFFgw}-D*67BBphM#o0VI2HE_K8pO$SYE7We!)qanrhc{la6+yjUz$ty^#6 z;5WH`nx3EDw zMU*-OBSMLB2K&7aeR%Q*O^$B>K^+z@v(09R(-0A0=PsQ8?YF<9>()7EM`dOrK6mas zQyKwuapY?NJoL~*7^{L0VwnG1)T&z}N#Uo=jnE{nZZLO(21|o9QJ_2e zDMV57+RT_!c%MfZ8;7P zN=j~4)_VZ97fX<7-<~>z---&zGzUWx8KB8F(!A0f(PCR~sh+M&tJMX+x zMDDxqBLE~d4mhWFwfJBD{@-Wj8*aEkM8G5>UTfR7Kl-CTT5CP?)JXvA(uZ$7@(F0n z{D^#`a6V2N>5vYeA7kQuLKv}xA)2LLdjMqG2(gDtG+eIa4$sNm7mGzH<@}jbrIa%% zh2w^ZnxpG!`z@e0)1T$0j zi#t2PI^{&6bD|!PYAL0Y9F#I(rf_M+`tAEJrPSseGPZ2nn{K)(rS#K3{nII>lP6Cy zbL0$dZ7oIQnrp5prJOo-2Ed!%^d=&@{`%`{t)Kq%BSiGs&wgH2pa1-?bN7cIepn>^ z=5O8w;FF(xP*sl|yINIucJ@Z|!+zig?r54O2&T;Hdfgp8dPGEi>Zg8ku~ks}Yb8QXZ0;vco(;D? z=QI!;ZEEc1?%ugffn-us)x;7(U^6oeV1)=&lv+=me5%%(MPh#ekEKdOAmf=RJiZR# z4J4qZQEDt`<_xNy@9z`cj8u(5s~}w&9aQq;wk9p?gAl101prU@xvliYY+zZBC^@*%q zrEDI5{E1qtid4NSh>5aNlsasw#{AF8L8UZJlZk`vt=3xlp2%w*!41A|t~0X|i%7(4H(fW2 zriaTODK)DV0qDA5+GLSo;(}5y%-!GrgO94}$3F4#uIrjKg!*Dd4a|GYt)z*uQ-qew zr3TCDXnHkG1OLN_-HOQGx@(%IZaO00-Q5L@vbYZfhLQ#xNjytR>Eg~#tWz_4;;|<} zz;a6NY$HcvNaG};qd+`crsV7co#hFuQLxy8V1`RU*&?i3$7;^$_sFflD zSSf`B?|a|-V*~)J)ly2C^VEmf6Qj~tOm!fyi6m5$s&;*kBH{Ha{PdxF&hd=z`r=EIhB7(lOIWf738vz2YfP~iZ^*()tCY`N@HQtODcX}Ij| z%5u5s&!4~WRMP+(UL3ieguAY5GL|Z=t*2U;m0fyE1adI5dU2~xUP{-da^LTL@bgdp z;MUDs&->cv-}21sE_}8umdo?9`o?d5O}0Jq-Fx5r{;np=yf2&sr%rKCM%4!X{MB3k z=HL0g@Bh)Xb@Srbb=y}TSuF*+w*KPJ{Kh?h@aP3gH#f5N>Q}vT380>R_g%kqq1){F z(ukA$s_cFA{qOz4U;o~%XB@rmIk(^RjGI6E*rsN#ocanh-nugaRmp%`AP6=W-uJGz z*YzG~t6j96^Vzk1>T@SvcI)#bwOgxZA(i&dY;UzGHG3Vfg=!^jEpXB5rdy=#-qYUB z`qa6-8@5(Ae)S7}{2%?(fA&xQ%@>|{DwhjwZrEh6E7Gi#GQj|?&`J;_7$pa)uC|UY zwzp24c(l~bMv>YrbrazD@uy7bx~q;oxz(NvsH2s>>lS%gAZdC1HP3j~_kaHz&!0N+ z+yCLd9pmS{(!`-gf#zFUzZJvHXD_{Qj?f#?g=6|K0~a@nA>!Mc??^ z*L~j)-*ny8zxivwoGW3m+BNWERW|+hqB*`XX^(BU&G)?FTjkp2z4yNJR~h@Bd)K`QrHtlol_1@hjf)mbX0k z`}ckHqxbKr-tw$xyx^;D?t9zmI;&1ZS(05zL%Gtv!fhYinn025E)FGPfjg58++98* zhz4$V3m@W`UN9c{+})iCgxQ`kkDyFmr@sGPeYFrz|N59|#F9L{_Vg^!i-7>S-T~ zLQj{6Qal`

    7^EsEk4s=uen?B2m={U6fKqB(h=pelO=N5_n!K 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%0D)+GZ6@yY zh3F%2dR>0UEI(m%AQ0(WocHz^dm-;elmw!qNQBl&{_chx2SyobkYa!Mkor6K-AM?g zR#^qAL<`2QjLvujI|D|nXU8Gh=KuIUvB<^qY~0{ z%~&Ilf|>x5RgIyC51fn$TOPw0BB3-hk5tq*Q@ApS6}|A4uOlgZr}2id@3uX+nu-%! zQ8JDwgJy9wYP6h=_GJCE0QIV~By$GtGipB`WfX|Mk$DSlXxMVSHcIM;c&l#J`0a4l zUz&Uraby|#3?YT1GSJ{!d_r84q&TM%4XDSP903ecK+Dc2PDT;;`lDR-^{*V(bB~!I zRm*4fgvsPP2eJk`uM{PAG$Zd65}c32!9@E^xs>_uV+Lf=j}Zg2Dn0XldN9UFd63oG zw^*T|pg>%loj*GgQ-%UEkdeATLRLe>z-lVSp(N6=UU=~P>{nm_FVom9A5pmN?d6mA z2`7H0g0oC+c=y7~&%ff>qQP{5fwdP(2yf8pVj+u)OT%#@`=0+F`x#hB;;MsH((Bp2 zJvYoX2qUeasrp8-K?4Jaf4bO~He&8nm?Mi*6$>!0ni7>1ag=dY?)1JQ&|?Wa8Rbg%lHH~nsf0Lbn>ZI2}) zr^E=laW~=71#AeqU3Nz!gs&)@xagw7O*{cniQ!cN;kxhTk8!|B>!t?*+jk4Y2*|1H zwmo_Qf@GT$Lgq-gZ}zpv-mI%55^z3aH0qLh|jP_Qvb=z2~qdApNf z^{b{BbZ!{<1ygAOu7`rrXTq_swGz4~h~F*Q#`QIEX*LSBP_6OE8NW1GpclMYCM}Z`Dn>0BZcE5m2@}T!jHo|@SqeVW4O^D zGri_pMnti6pNSlcb?nsY%pu;fFHF>cJ1ETfqZ(9wq}mmN)kD0Y8n5_K%1ltd(OcxE zV`K5De)$AM!BLrs16nw~Hm#dNbS+tHl)ObK3r7dFJ*CcD#{---kZ6YMtf3i;;%roL zdS%JkADxwQOxspP>r@OSjME|yMzL-x`YS& z`fYo)hg*7(OPInA-&kdlfa|QYZOhWss!fH{1_3>V^$kW!d1VYIg=rk3!gX>37)fw! zH)kTug{T0xVW!oPRf};vpVA0tN#+BGmioB1|1ila)e7V5>5R}>mPg_1Mp}sN+n~p5 z4{3NVd2es!mOzuAx|N3Y-0{{=3a{O2 zG;h?Q+BsH&&9UN9gh3TUH)8X~*N@aM-)Ank?tJ~ay`)|*Yar4?Y&EYmgH0Jnw118A zkhKMp)1}MWEZuX%I07rt5c!xgP&?KM>xsijD@VqZp%cr!o`%!gYNWaLJOblU2F}-M zkG3nMRZWlku5A+|inM8c_HU{g3|H0*ss&Ab)N1g*!9Kg?ckrft1y-4U#+D6H#;)}3 zjycy;w@KX(b-W!k-7IP)b`4oNTDK$?R`8tuW#_!yy=HT$Zj#F1f>g8kKw|IwAK*zO z53#r}Bl8ANvx?blAq6htdZ-cb$bkoI;esykF<=!bHs#XstR9cqxxCZyFq0-*wbsVG ze$U*GK;e|N1 z{eLJ2G8?nqb&DYtq*2xHf(+oi4hI5BryC0~T2Ti3yeE@HR7g`%*o->vWH?}Cof<8u z>tXm&Tx)@>ky<*l${R`2NzSvFROZOxfNFP%Y4a^qs2o64b#ugM#aNZD`IR7 z$mD=6!2F5)e@CP$rLxpt(MUnp?2!?tvhunxersI)kdUN54qyYFfpr?-D1ag&ptP^g zl$!|_L+mJ}Z^N9PuLV-%$+&{>%<1-%Byi`bTnaor-5fsrJX?gYS8{wZQBa36DBv z8IR;q!MnioQY&ieGri4Aqw+Ro>fmUKx_m*GMlZD~b&2c6T}$~?{}U}IBFv0>v3>&S(x*uo5B-k!Q2OD{g;Hg%1AvBbia3SG`%z=sbs^Fsn4583yR~jKY85Z17 zK$ac%a)+H*gpMQ+2WPLISx0%NXQ1kNM;r4Fr_33Z-T)gGDD_!RlVSO}{w%CX*6bqL z6vYH9rdH1Y)sjrbcK^{&y|4CEpn?8KPRHm5DYGa+mI|G}+XrEGUVLN$S^z8FmY1S% zpMC>jbGuL}M1Alzg1~D-SyuYm0))z29)~7-<2UnJDrAY>a&vNI;$GFa4DYTs5X$Oj<~v)=XY`Wv9Mrp6GlkDN-ecE&2D%_|d;G)vS`4 zjBUL@p}p)1nvT+FC2WS8WjC=0%B1|+~+IVnpe1|ZTkZHO|v*5fS1vr zOg{ti$H@f@cv(wn z91{p`oDa{z|L7oDiSQEXAYk_Ok+<2UcWv@f&XZRmJ7Fl6$e}pnp9msR6CV7hFeRZy ziKK{R6qqCS5SSQa$pO8{jQ~Wow9%J9+7({2Sh(ytR8@CjvWi|V4&FQA(S8oC<)>&nf` zyjxd;TcD(5-);tU^KnA9g>Kho-PVDii@(R+&&T|MfHCzM0#J{l0UGoB&O?QObHacx zR5~?_3?b$Ia(&X{+^mD&fY~xV)ar+4p`)5+Vi;@#m@I7!Hi4ZVv&UO;US3|$mxs*& zcXxMS_YubG3s;|T2k3JdNUD-9ZQYDrfKg7KMdNyzu>0=-;7;KleIH{-Dcw!L-!Xy< zGUTea;OFO0B6VezMKGi*9mMw-lhrvxQN$$x$3j1n#755FO5qLbYKmY`p7X7=ag6Y$~WVk>^Cxv{BmEG_jAU|1a-MMbTF0;ep@bFtmnVcypWHQS zwHek!d4CV|!c1S6&UkE`70SgZ9nZv$(D-^7{y3#QmXs#L=9Bhm+An8ymTO7EC${8R zv*&>nh;x8&rU57k=X``VKlH?MOme%4+DWV5tPmb_Z8e9eHVSFZo}IRvzr#?1yahGl z@LN&^2^vt33UA%%h)QmFOQ#AD51MF4iGy$eGexCiJk1}HxAnH)9$i^T4e`Lty8dI# zEulF7cy`vDT9Y$F!n4wE zd3Y$XnTo|v!mxkX(kX>ya4OC}j*1+P;jQkAwgye-#SXEu@Eg?rR(xSpmj`^Ouomnl z@N4I~3f@{?&fP#+Af=_HwxjjNRE#bN$#dW5t zY+kXYMH@ZqiYoL;eSM<_`I3pkq^8H&fqhiotWi>7w-O#SRr{U>fs;*CSZ=w^2mX?Z zeX2+!;Sjxk#%R7N%^VXv?-wycE!8#?fWQoRbs)Tvh;zmV{w4SX1SFuwVo=h_)p9=} z&ZW6EHxK~t?^uUt99V6J+d(E*0Vv5H3n*V~f{xeMlnLaWx)n!%~ z!b8TY9}B`+#r~=Qxrijnvhms5WMA@O0gE*cL6hM#oR8D7zEV^w0Dd`5tWvSTxL{?6 z2x6CtMFb{>BGS#*z3>-{FQci9Qk+`VYZXJt!}~0$*5MgRS5d=e@j@lt&6$|**l)ze zntmDfBm$GxCEVzH_4vdJ6TfV+C~IlUxUm7%JJQ~3zmxx1qL!Y*-?1tk_*o}t$UqcX zwpgNbCYXbdaADr+OZjpQ`mwq=78kGa;XrJ|L_@hrDikBOO%PS2oUe-?zLyzR9$&+O z>?geml%+LdbAAd#-2FB&_DPv^MJQg;#}k5UHN;zUn1$g%a5ygfR6-fGs?h)C0))K= z!L#F3pkyj6S2KP`yV#$?W6!#kfTFhlShlRgtfA`;N`A6h3 z0PX=`N7TOF1&}&?fFjeaTlKHt`2%E-w}oFdD}Puv=vKYY{*k?L6++4MyZhxWoHdcE z>F&$T=JRI$U1oa6p@P%>tjLqv2Le2C&%+N!ZL6mQUmpHJVW;lLRT!1?whgOtEbukG zJO4Cc!V3sPVHgn%*w*jZD?PhcUscWrwHm$Z`)sJY38+85Hbyd(IDMmxl&W~t%B?pQ zO+|%Sqot^qP8Fz^TWn4bjf}_8RnVe|UHh4lMiS}{w+8G>K{RcSHTsi!lL>P&f)tCb zlLS9tQ}FAK6lECE??{E=Obu?~vCFUoWSP<y%N`=7Gpr;ED@h>_hbIGV(R12PUL*32iZY?TkSnQ=ixXJx{LaoO%~Gs<{Hm9% z(-*{tt!wI9ll1$VeQTm&y#apukk^+V+Wq%oNSYcpRpCe`cb&Mur*caow;6CtSpy3X zpUdsxbP-I#@*dsatFYAh@2%&T)74yQ$$cCBX{2z*hIL~DAPWMbNkBw$#iz!oW#x2v zZsw^N?5#^Q?K^#Cmf6@*7*cTUQj5QnvI#`a{N4;&cBfR4XW& z&jsp1drV$%|BV{>VraFA$>yf&JM&N!EJ%Uaf<-|>T@xh;bapZ1ZJ&tRU@L!MxeqGX zkjXxN&zax1njgD|*9|pThsN^f5nj22Z+3f2tC9=iD>;5Ru5e#)kKz5G-s`_eY@Qs> z-M45y-G4tq)dJ>{e>&!GvUoZE8rD!w%57@h(_Xyy5XKv62nr69*W3^Z&}-62_nE@{{1TGbTmyLniwCVI^(Oi+=+ ziVsqPCNdz}kSUTeeok8`E6!cb=Gk5ub%2Sb(zrAra>e+xgME~b*M*ax*Oj{f&fjC- zREqH9%;@L-+RPTYkX&qxE_r2Jhe0(`vtA8fj*ZFWKG|Np^~Q-9qRNtz=b8gQu|3~E zuA>h`mN$9$F~TIp$~B?)MZIGOtT=w!a9teRR(FxF!c>2N{0xu(Bal#0XOU9rOBuc< z7;DOZn(`g2wJrT2_N$w*!SZFD7E~EhJMK0?71 zCYT9L^yDfx*h|s2H87ogm~MN!CD{HaMrYW=x9)u`LJV#XNa!o?SlAQGTfgpote!eceKm)o!ipLvj4mZC)xf$Z?peN~HImv>SEG)L!L<8eyIaZ=(wcE4PX;>F ze@9I4WL>XOuXCI$=|1uLZ<2T-f}*S|vB1XbOWSmd5j~vm@5))*(^0ufUG`P?MaU!G zS3ur`!qtk8{JIz%1qe^J;&dR%aOQ^z+tg_9de8aw-dBbAI5y$pqi#w7)$)}4LQcTq zUOB!q6eEpC<#Y2!USCei=L0+&L2S7FGrd-QUUx2GGiRO-Ro^nytyEcLbd24<)d&fz z40&-e+N-My2fuB^>fMBR;Wlqy zf|70>GBu`d!>LXIx-`2UBJGZlckM~;U(2tYs?bNgKYj(I?xkNY*L1suGZo_hlG3}G zTqCk$mZvjwX=^QOa?K(OjXJmAJ73u2k70QyIFGgD1~TuXZ?jiC8XKBrrf{wNViB$F zR#ipbZXv8aYB&>ssw$tWnca75*j3q;lrUTmznVoNuy>Y~kY5|B!(!EOsCF(Wb-s(Y z8|XveKYqksMs8BWr`zQ*Uv7;#YXkx=Hb<)@0nK`M&rB zIl~y1f?k2b$I1gPi{WzAhV75V=~&=|Do&MQL+Dw$DC3=PK=4}#JEt-5TdQWBZt$6} zV~>%R@v$!*=6ybMf=ZKYT*~+h$R5Q;RvVXJeGtNa!uGIc+IMa zyCTVP8e2|^-u{hDSf{U6*3|p~6MOKVo+F}YF66ozuK5`8EnOEj`ab{k1wNmrzx?s+ z5*r|Xtt0lju+s0)q3^jbX3zzXRL;-OO&GjZYu?vtbi2L(qr6tYbXP8zI{{nF(y8?2 zs$vL(e&FwTO@eVw5rKOV;`d|m*w|Rn{o~`~Fu#9cXg9rIi94SUGi8fA^lHF^S}-p0 z>j?ZI_#Sb(QwcxZa6umo&hwfST>i1#I(`M0QJ6cwfkgx-=ZbC1_`&V@xpkY4)qY2j zA!YW9!&$$j^XTb_3abll=pYbFhe@YAF?CnXi@R#N#a|ynO7|wx?eMBDYH8QPutN*ErVp*YG1d8{^B8|yqM27=(O7R zmgMPGSCj;J+p57TT0=;I6sUrCxA=E8?9qZ}bT>ly4kmDC%?ie=Y76+K2T&|<&$ zbRQ@Y_cnJgcBEC2o$~zlS^{nj6#pXw$_^D2=QI-Oia&5|hJEe9xQnwi!RBbVUa5Uu zTqfV&Jyq>VzWMV9qf;MtVE30_H;Byq!xO$@2O)WNu` z%%Z|or2dg|8B8{c(OGOJa)&y9stsxT4wjycS`Jh(7+C+X?lZgQwzP1vqjXpqd}S4s zAELOb@PLP#S}Uc}j`%#xTL&H)OK*T9E`u%=#G>rO{kvB%xmgjGEbaacMKp=>pR=An zMzpQtL@0**7WA122*)ttlI$PTapXE$h%PRb2uRTJ2T|$5SsZQPzI9O(LAQ^OwZy)T zMw$*{ai~)g1$H4+htXaOll#SETU0@l*}WZ!@W3qBXyN$Ys)V|jTbu(n0G2$^Qce&c zXaF)k9{#S{DS2(v*BU#6i*G2-mGfspbj-EUIVIfu;kTXdU1r?8oh7=&!JFsUL$t1! zigq_7yat1!m|6H((_oz@6HK3o0iu(aJsp?tpm9TL7pXhyfUkg&=Gva(G|`b02?y z6?5OVwJIQ-6Om~Mw@(| z{QmU=uEAXT$CBOql52-zgKV_op2hFb$I;PU09@;k@k#v-tc8iyB#!eNU(K<2_BXN)Yk8o;ps`%;Yy?h%)9t`hJu2HgaRo9C(on*!e>7rGj9gG>r9)`Mc=(p&Aor3Al(!AuVYh7O!ttZV zk1=Qs&TFYDJD_Nz*)>{u{M+ZRU!E=gnS7gWiL;-%C%cCQhj!s_@wALYLw&B%cPne+aVpmoGxzB^WpP-{tL&J^U3YOI5;UH3QoSXX=G(* z`}}w9kBIXZ+)f$RAwr1!*|c@Ln@pwSNr}`D7ynwh7zkUf0CN|f&aD)fXkrBi6<_vIAweq-nTwVo3>DAJ;Hn z622wFH0tcl4HKHrUFJ4w{Gee>IN>5-9z!uYV#`@&A{v*}Ysh0Y0~$3VJ%Za5IsXeu zS5Wx+MN0bj`U=@QWI%x8J&<~Vu%h#^vK$KfPj#yaeZMrtO4RkAUFDhMd$url#hVbv z$b9g?H$@WWT`~qJBS8Xy6hg4B4~JD6X4?59?SSnyFyTM~*bfI`mWll3Jm)+u$O~WM zTAbv8fAdJUY$}oS7*Q3xl~4pZd3fXwtnJ9DN|b9bi9w)NCo{n1t{l9c2%I75<{)91 zD?))_8G~&bU18G_glWFxpqo5NN?PJ9j^O-iKQyvMFiy%_SPAi$D1BE`UXK_7w?$eu z$4M>1t|~u#nqArW+DIn6*6JIpWjjrGT|F${J4YGGpiqwy!qtBN69=0fj3gY|#nhRH zq@c4-kO=sM6w#VRfP{Gy!qCA@vB~f^_yP%sV$6V8U53U*9>xwUg38LuL}BL1OZe#A zQ{y$wrHZ#BYAhydy2N$$AQM%@ie=sjE_9<|VP~QR&0Iy1F{@SSn(D z0F^vjBa_-;mQklj8k4qjrRY zegkBg0fzT1TsrH2zmL@)E{AV&>|d}1tPLpB72nP?BC1_%URL6DdTjL^KmfuU%0`4X zp1TisR@-Y~!boKjV1AfUDwG$%k<2MnTjaFi9&iPNm6B99XA*%~e$ikNJY2_avIz}4=P8P3OVK04gb8o{X;;6N z4vT*}AsW%x)&_y8BCt+U{`ecz#aX1KSCQ%F)VZx+0IapJ0fyls6Q;W!Im+B8Eo8xY z`ZkK=b{E!fwe#A9U)$~nog!Y(MBIl zykx9OVM&LMQTFE_%C={iiPqRo#5<0O6dT~a`~3icf}{L^P<+0>#-8w_8nYmv@cBM1 z7(8vZUzju71yTF1-=4b?bf0pmC&7AVc-{g(R55tSL4Ghz?)$Eg06i%bUw4|0!!j=! zh$?!IERtONqR=tJL^1s5p~Y%$ZPt-R%Q}-2;aAfo3}Uy}`0!m4z+n<+M{4VJX{3ZM znUn#oi#A&` zeZz`|R+|+nJV6RVd*?k!{7l*9g;k21{mIspNW+n$ds)xQI#G?|yvKP;0d6V*7q}1N z`WQ@Ga7T=++x1|LjdG0PMo#$pX}nQ6PBFXc1tPP)OKnJ6h|x0K*GJPNRFGrQt%l9B z9w-o`hS3}ZnYp;Y<=MU(gYN9A6!e0c#`?*s|Nfgwpd0%4zHFz1FZ z9|3G22~OT$RLBtTl3aY*Thmn4{vuT1#?q37zQ8R4 z{ugeZKfx5{Bz$~*|M2Gj>|=QA17n&D_{j^8V-p>lm)`tm?A*cEuRWiy0d0wl&|l4Eo=WZD)@qj;M!;FWq}4bmNNfEq>~xIB-HSp1K}_Z z$`i1Fs5~WsE>`dcD&?E6@$2s|tEmicB2q)dpY0G> z4W7jr?d41`^S>O&2ZLoFjPc?tLmWw~C+UBag;mg`(Q>^aAmae-GkJ36-zGz*vsb4u zdMhQd5T;3P+Bb(Pcj=|A1 z*^xG_W{U~^`IevJrYwtVfs;-l%g2M)z@Xun*?mHxY&DJoYZC4P=QmF2u96W!Fp^}f zw|toG(E9hK4g(pId(a(1(X*t@F8h|MTzB8jQq7UJ-)t>Xiib)+NhXPw%_mmAc#Y;s z45m$vrcJ&_OK)wdWdE0Z>4>w>2Nc!>DW74VQY=U-vqnZ1u5=64zJUvi%o>Bci^uTQ zYtbfUeXsscYV)ufb1emi@;xY>g$@iKB;N?S(G&% zo4!@6c6fpBR$rnoe?FV)d`NDGAPsEaY;Av|z0$lddmLyi@>u_;iG$QzT*SuV!&YQv z&ND4}O6LOzao{LkHAJ@F-paaBxZxV?b~xb&5?I&v!u${`rVF&Wai7eAO_`znp#p4Q87$A_GW(v_D)^es`qQW_HWLML`7@^2kNr+_X zBdehL7@1d$CSKN`r(|Vh#ZK%Y6s9m+j$xwF)62_?_gfgQ0N9QPN0t<@5P@^T#Cx1M za+Fal@T7#X!~Huyhn8MnUmXZ9{4vnDa;K+r1^gb*H33WwBsjC&-(ve2j{}fE#$N}m zA_xHZ5`zbS8QkuT*=Ex@5hI&B*e>W@7Rate zk;!lHq`bVm;K=p6ub-2Xlb;{)7&Wx#*mxo(@zfn*c&qgp+g#%FMj3I=+OEnx;p}+{ zX!`|RmEw#I8n9H-uEG^20W^NA+BTPsNjMA)ltrsZS{XV9l=gV2Mmi$*ww-YRj7_3s z@b(gNLbPOW*-}UG zsEU1#0P9(&Lc0n|BFoW2oUOc{B1VUdjiT)L+;uCx?4b=~+%<|((ze%=+}1eCg$bAL zyd_C`UKebpOVOwZ!e*u}9$Ju*YfoTRgzl<0KlN`OzWjZKpn~R=dWvG+tEneF4S|jN0bENJ9|O5q#7a{0?T^zal%8kFlH|<=;-R*BCgn&P@PFV*bIYP#h(jNd%O)H4g!n_RRY06=r%Z5j z5Yg%NC-+8K_&2Du(IPHVe0YWAs@J{TI3(ozSpLR1g*RTvRbwt@2g8(KT*5dTn%Jnd zmWwN-#P2p8F{Q`0W-%EW7-wBJ@Dt%bHPV1P`PGhm^8%UQy|0_1`Q3KElem6b{fToa+TpyezOv^1a$mR@`zz-{CzG~5C5|<;zOH=$MN`_Hf9Lt) zf%XG=L0Uf@iTytb@c-UC&8M>%b;}4bw=0(xUz>X`k{j!t$RG@1ffhMt{t>nij7kH4 zvPQB&G(bxwvmAhIx4|40cV+wn1{IuVmB45vKZBE0aZ7~Aj8 z;bm2Hmy;Oq-KKS4AF#{D8rLsa<6OavBVjD4sBp=ez(puqj*_V6a(3~p;sf0!a-~{V zJinT#;c*BT<4DOOBmJ9^${^UvI(Z&Vc{HdI;N=wzT=QtE0oVQzdbqijCaZ=YyYupU z-XDYi^tgX0>Hnen5>a$;--6AiS0zGc|B~R!wnXVRSKu5FQA}@sh2Xm^w+#pjt9)8v zR1X82?t4D${Ziy2-atLnh`zh8zc$*CFb=1aMXkj)ig=`SiJxvZ6=}5nS&Jek0k3+8 zEvxIca6{H0o>leC5&S=P^}2-JHS0C&`-vP|K7oJn2`o@&xh0c~1EwPX%K|im88PQa z|H%$4KwW0JpuE_2D;yu&-->u>t0h-$!yFx#OxpPIrL$tRF6a7}FoKHim$NcmYId|O z-R0=^TnF*=6f6Rx>Q&H0o5>Mf`qA7s`kF@TlR^9q-Z2wtFdbv<%UtNa-AA(^hu?!< z16OIN+&Lc7U2EYDGXMR59Vr*6UyLhmTI`E+yZ?a>Onv$@|#NLp(tgi1jJbbot#=gvV z=&5Eh|8v~BQ6@dUPq}}-igZM2SvuImfhDgNVwr2{9aP!a%BIfAg!I9BCe``=59edTZQ%;i1^6j{@I z#wgctdpHH{T*|qT#zZT7hgd|8aYqQ5cEp-XEl3XG%=?XP#0S<0?kWRff+YmCFhKCR zRs#lhl_o#7T+*3oMkC0cJ19K3mXP&g=i@`qsH7}u8a@8**_)G)bwc(Xn1RMTea8rX ztY&|ddl^<;ih?|0J8cV%qL+lD(SGn!!ZmQiEs)l2UoH9+5P0(Z)AEv`ws5G*%*^!2 z88-0a&SssGn`R>gE`s&C55Kfi&adG@{x9mDuyRqOe~-b~-FyRfp6>+)j=2sR ziB_Y}cf5bj&2)qTED$5#^NWg~X$(_wwQuU|(tRA@A=4?x?hN8j+hjH(e?LxGY}SFf zcGidAWZgD|%h0nBMAw~>nLB}ynCJ{kFs!vFXvlLr{x&z4v z`SCFqIg{!{h4;kCIKG@mJ37~jbW0f9<=*VO{EbRzyX3nEZwBDp1tH7XH|Ed^H0Szp zG2`+1Z)NE*%t6ZjNkx&n`j|br0FXU*7fFF*Lfn*BCKjmpTUQ~>7vbk~CJLn(LCN^E zhZSB~6WokJ`l-62Ob>!z;e@efnXuE57a-0J0HBwa4S*abl89nZiGtdi)z?l07$J~? z2^eu%m(KT%0pXuR5QGS#Q_6DR*hxwXEW*4rrI_Go{5;?ErNW|}1POI5m6g$eRz0kr zzf(Z`P!=~DS3=)JF}L}`Q2Fkdx0^oG%9`l)SQFxKj9x20$}9gX+J@h@pIjr63%*^k zjubZ(taquBzWfk%$L%|-@uL|kqsd>mu>s!RpWGGK=s2K3k~ZSe{NK|-APvS~(0o{f zPlMvlOH1c$Z?#7T1ic(==!!!ICS-9^!i35jUeetR8-kGPWm)Km1$X&@BB;dUTUbEd zxpS9R2!a-#KS*dh(j=1#NDj5jEA>!VMlfmtJy}+KOQWhPo1xnpUPfOkm6QVhdk~{p zI7YC8WjgH4i~2mP1w1TT`7}v!Q5!XbUT14Z2Mb0-N_o5?$E^PuYCg(YC{J z;oY1@Nv~_E_N@vePrp#wYX+{$v#EGy%*n_v+i+&I}2B>1SyR6DH1 ztLai&xQ#rY6sGcfiLb_`pl8zdyF+(}@`SJ1x^ZWxASIw^cnLGylvzh;LU=2wNkB16*04; zb~y|mS-qcIlh8s^YC_m7sv0rErmsk^pLj|>DJop`t;V$0zETO|SBBB$6F^P@goBj&3o9rLnc|3QUlHDPqS_?(Zm<2;d1G>T=YdRm z{^T{;I78G{A=uAY1iog3so0Xhct#ud{Z9W5n1mwHD~I5_diRd*7ZjSBXL9t_P*!bZ zOl^)(n$_zf^KGfhfe}6Q=Fg-Qv9mNzJPen)bN2G$nQl0Ua>tV1xxAcmMw|>%{JLVs zf0vHh2Tt_$Ph?Ep{zrvO5XN;;z^lB8tT{40N4SiyD zDnJ<^g;YZHmbR!V3GL1`ktg&LY-=}e1ldI{LYug=D{p+R+H0{~mspiYo6|m$5g~yf zX`J*}Fy6CiIq%KC%pBilu%(#&{uj}7XqG?0c6LlaoMTs^N4Cg)`;OmV=De8Tb1J-b z^)&GYP6;ScClo@%xm<$hkSGc%)85>8NQ)O2*wC7R|JgiTSg-EuDg?p^_k=1JQ4u*{2j0elr7-pQAXAR z!{iyS5`fioCOJY?3iTgZ%4~s69;`~N?}tL*^tuAN!O!dQhMlAC)fWo7In(l=;@{+* z7~Qp5&=fUCIQjxD zYTU*(`N2Umq_8of12il0s!1Ft7iL00%prBZ<}Lr7bxKY2Wtj`*#td;WCnef5uaTqv z_k|QUG}4MkiyS2TfHUhK(4P`eXvNP-N}lfS3SR9CuVw@|00k(;l(}yzJm~(C8n>nF zJlVHR{Sc+k#^G(@!KdLwu^y76md`P-olJ@M>YE-H{dnwaLTVrQ^ z_+gb@=Xio{Xy;>RiZ=siH7a8a4Yc~!^QlU67<`58xzcX$`$}XQ;o;^SL%4-QE31Z- z1-Nlao;Po8TUk?Pi@_Vpg}F8Pr^ponAzv3*=4|4>HwSKl_DCGTF#1xlhfuKbq9vsJsy>X<`Di zX;!T=D}6j1pTEisve_!87Rj|AIq^A->@A&n@j{SG^YrdAG_Aw zeaL_bN<3^Y{wQ<{nfc??0c4IW#%OhK=$r+br$M9>3=J@+EZyMJ?W@h)so(lJ?E3;s zP+en*75n-goUM`1OTTj&L}VtR#W-Rc=o)X;)_sGT;fTo!bHyDgcO}fS<*6Mcov)#n z2GbWZoBoa-WIm|CA5gt|j^LDvaAPzt1RjIe6pPXM)gIoG0x|E7D;zrU;t5m;1U_iO zAEYB9V?JLxe04tL8C2f;$G-$v>tJ{MoPJ%>qnT5msixDPTziVSzcZ{fYI-_+kaoTk z?ssyLZMO;zIqS=@9@=6oa-;WTuI_G?6EmbNuE0AR4l!)e?Yhb?=Y#assSMV@U_L{5 zQ_b7ylG<=N%J?1WW<*K1SWJunnXbA3+#SsBp7HK%B^?2 zwTw`f)NxN+@QKMzTc%n19Kc3MfUP@n`49Bz2z&q5rqC~8Im+s}Eu6<3pKZ`CB=*Tx z+7Yi_=UDIhOr-<-`2I5Qs5I^uxu2My~G8(Guyy~b#b0UXE!$v*?Hfd%=zb=!ewX+Zc5j>)L%AMT%LnTIrB ziHCu9H!*>q%ziG0mak!f|I3s1RnX?r?!w{#_n8TZV{qu`@?%%y0Ub^>-Aw&HI^59ud;Wt7&Q`bGvEqdrvyjq+WGGx}202hT7!`G| zOwNbiWm;IBePDjZDNrx?ZBX0(&gsI08Y{D;WwEcy7IRhDHvK4DaP&l_+=`O1@_6$C z29u+$)%**a`pFCs+7w0_?kg!4mh~t4|;21F1OKYlO= zr$ISoU{QK4GoIrhbMgF+|CObxl8phqKIXQi(Ld-T#5?(*rHp~!Lda6eg)X{jk+x3> z^cRMW2O2Y3*1$v^J5L-z{Uj7O{kEI;pn^1g`<7sHax@Yi)hF=Xx=%cVY+z)9$_rph z;qQEPij$Yw{~=vOSnNR=)*mpwL#)Ie!*1faHuHYd>-s=p{I=qQ6Uqfgf<8Vf(@ZFH zSVkdynYk`O$`?mQxyWfzXtlvfY@&~b3V~>}Dj@DsYp%|PdgRP43J*4dK&%ZmU}~@4 zV=23NtP+%Tv!G|{me-YwFRw=v(E>!*&Bz;N1$u(p;9-SOv@%Q5(*u4Q$oRGxy?!^P z#}np)K@jX^pSI`?E*Ztuhyj5u_H241(3=V(50w3J(Z} zfg{b5Ya^+phhTVA4NY9VpZ@Y@hI4hIXF$8)?kBZLTZ!ayV)>hdX-7`az4(+Ju> zJQM?+(=Ktoy@lnSY6KB;h>!n|^JiTv^i`EYYE`sq{bYpR@?n(ye!_FERpx}y5lzDk zlca=XKv;+bz0pvOgrt}i0^(;u-uF&OG`M6sD=hz-&y?`aF0g!+w|x7=RLS!m?r{E!Nd#Wu3Z;| z;JcW>_-)74@jaa${!4^DJy!-FN*0gQ?J7J25lk7ZDtgRcRNu#vjOP<$5=tH=9NeT5~f9;h~^7Un>{%q`$3%mV%7`Eod*A3?mibw0!L|>9o*L}^QBM^Sxc>>)*=?}IM zP^hWXwjcbQ#9plCGjGv9 zqE_h%j5{w@$gE+|XnYO9^A6hYLuHa)F&cMrR+y90sP$;)(dctn zVAfZnFu@#$<#yNf{E)apvV>CWcdHcSOQW6(rwfx)wR9*cfbcE-N@c2cL{vZgs=&Qj z%P)x7v}Z#vQkBdO-}jPOuWNf|NjHjA-`-aL-iCdyW=DG)VHvLcQH>?y{azQnc9QlZ zK4fwue(bDO0co~$|3CqRud>ayt_rS&E zo6Iwuqn~P!CF*5nSFoj%nTz>jX)=n&H@5PGs^`>VNMI~K&S&{y}CBP%+Y1pUW}vE+rW>aG#C?<>W| z4iBJtt17Q^_4J&UWd*^o!ye@_yjysw|1-gI*T|ys_3L~oTZf+VqyfEqK_1`i4U^6= z!lXF!Zk4i5c7AaPN0pplL9=RH=5>PIncdSCMkKW_m_H#8G=L=yez(QBg`>LtcVh(A z?d1)~J18nS8JL*X=f#MBW?iTEAbbL~**E4)^27OEI_18mg9DPpZ@t8C$D@h*uT7tO zExvnE;6_{Z9N+u%$Hx?msiQma+4nB>YW?G4dz6vqZVd)gU~?G070tK&Ihx zq|TLHOaQxv+&pbZ+pdcZv46*Yra=$*>m3BDN8q3g zZfH4Zh%1}XGs{J4gGGwiCsdITpL9>HoM{)?+x8kKzE28@+^wOqPl)_-bMd(k|IS_1 zExG7fKH{4JD@`#6FcZFF`9L5*;h8P84!Bu>_|>bacSHWt@w0-~3Yr`?agD7@cUBHb z^V%R`(A-NA+hYNd$ng<*n3N3YaX)gUw$hM#l~yB(kN`{! zst=w_CL*=axP1v*`Pfj@hhKOS54|Uv)Q4VRxVwCabtT?(e!do+HO?;jN@^ZSTgE=ERc27 zafaQl#p@iP%lt%#P71Lq%*n@o-nGwX~^0PUa&mH<@0X0Z#9oHjCiN?`o`;ef`P}w$`RS zEj>4sdmB&Thr&CClqeUo_2R3&@8+ZCa~-cQg@>-6sy2$E!Kh`!q`8^Gpo|*o?5;(s z84rvg8yb8Wso21TV-Zl`>KY{+Dk&<|Z?oG)MIj~aXRH-0ow200YZt~KcdEA81sl>+ zyYEcWQZ$RxlR6s8A6WYrD6Be%_MBx9>c!PBMY3z_mWHW8$C_*N8zo8%8Qy26lb#>e zoMxmd!|WfT^0Y2A`(NXdBz8gY5Tqo*Z_o?M-oj9%P^oB_W|K}bJ>mXXq|NOb8DB>4 zN>aj0Bf?8OcIY8A2C_87a>+B4^`P;>_unFtys0bHC|DgwXD&sn=rD+UrCebrVM+ez zG?O1pNB|-0ck{MyaQ7yW6mGX(bZ8gez*@|Mwu2&SNf?d70brn_R<9o+K@>4 zgs9wT3_d_l%{cL4kmZ4o(L_b?^KGF?TwY|@ArXyj)NkYQ5H&mZ)JI~gH7y8XS<<&K zXey`mh8|`qLF9O*axD!TI{xdWf8gYV%toj4ZC2W_y+%+(&}u(Y&|HrpS%iM=M%JZ; znKROB9vMHh9H#YJAj_xLW6-QDPFkamQ`SdO-V(ed9j5#4xpVspOQe;Y9GXB>o=U!Y zP~wJ$yz8&nJH0`&=TgVWOB-a_^Sq1z(iBq1izI|rxr>Fzqj3Lvj{-jusfs3A<@S5Y zswwDvYDvxD+eV$X4}YTggmE}$DNmbI=WLQhJ8wh{bI;`P{alMhaqpXyG=>A9q05s; z%i#PLpgb9uWB1TE{VOy6)sO6^bsMcsNiR-co%Vw?dN|j`ZkVW9*S0c|Zev@i(Ka0|FQfmL3(5*>XoXRE) zViF_9ukqs2f1@LdfJQ10IpuZ{&Qtzh7GPZ0zORbjRcB?{Mad9P>x6sTu3d04xGY=p zmlV1}vCdu}g)!aZV|XR=~&CflYzaCfm@L^Ifx>ahY1!_8pDo^E`>9)i!4J86Nh8=A2Y5LYwmZU&!$c>g%Kyqob;j4GOw&gO~sY zA(5D9`Dg5PAEfPB9{g68=?3U)kJy^o?T{zBjc%!y|3ihSCc~xUzXKCLjtq+O>gWSP4FD0LNql}c*5P|#copm)?;!~izm1B|P}$kT7JIsE z`HVe#8y<#*y`uK@_rG&a9i5N{0}|d4g%ASslj3I`mHu02^HRM1LWjh?72_ZfC&Lnf z?*o6^al@PKn)T89`O)KZ18!>pcO{@8cQ83>aHY)Ku|}Pi5h$RdNvn=-_EU1&Z~>`x z#)aCTeC0;dzl=>jsMEv@Qp0p9>iK!zO>#v1rz;YIhMlth9SMATn99vZ7r>q@2?T?Y zSM|?r0nXg%nw}4CmhFv=qNFm+Hxu=bk1@R-UzjNFeP7FaT4xH%8nas^waj8QlSRyq zk}|L2YMQmZMokzARkebjFY1@6PFVh-wfM6YCQl8}{;7(8s&-Xl%4XTsxf;w=$P`0H z*DyOHAt~xHVW0%tVT$AGSpUW(EFZm~tFt1)2@a@A*JN{~T0=ZqQ5Dx!kvTTZGeDeJ z=eZ6Wb#ntbGenfU%#gZF74`8YM#VK_vBifaQd6{_8`|}Gse-w4B|)rl%LNOgM43BU zlX!6IhjkW@=rwCicp!zb5h{HmXnGlXX`oLtRa1=bmWtDD zj<41m&p>d3N#>Xe4DLOb_Ot5!6uZ{49u@SJScqsz-r3TI7wA+?ZTuv?Ju7}_Z&aN^ z>_Z{^KCb^CQ)d~}Rs(M9;KAJ~gaF0etypk(DeltZ?%v?;THM_!#oe_?fda*?Xwh=> zotZQD{LY^}liB;VwVrjL=(H{_TLve=gm9}m;_e>AqTGYS=4sq42 z=e1QtuMnUvo6R91k+FU&kZe@+d%H?~w z!eLHv$mbAQv2^=(nMbWQWGbdek2fyC20;$3w8oQ*>(#+^$s3DX^e@oNJE1r9z8L|D zc5Yl;(MgDIi(+b8PR=_tY(-C3K(K|Iz0Q8sQtLP8UbY4fk$rZMnyn&$0Shdd%zqD< zkpF`2!~4X9Q4D1`RI+Q6h9#Y1_}s+fLx<9T4;bvWU;sk1=nA}e^XLiW9f#+>X-=dY zqh2HYJh{2$bc)XV00xJq<>d?XR8k+8&O%mrbMd9A06ZT~lc0veDc0jy=lqx`Z{|B^Nvv`22D$|xG}@60Rk z(93hr`$K`v7)Ly?cD&Do+b#Apw64S5X=D2NM`O`&z$e>v{mmdh!Z9%)yXFh|^J3g{ zT~PUf+*LdeA247c{zsd|AtnSeyREZqFpp~%m*^pg!e9#cKs4fke@0Co1 z{iBDNg3MJ}BvV2wH_djoDjvkeIl7JJf}ySS4@}9F+5(Q_Z(X~B(}NMM86%?8nEWo8 z5Ay92wMFw@>#^qAWd4Sfj_$65S>J^R8!cxYg%dJpNpMlyFVKq9OsZh9ew)i3a;KWS z>rRVYC&$La=dS3iANib&%9lrd&j`)>r(K9UIX;`BzOamR=-?cCE55rgb-_t=b#8+bHk$lwS$$RqnO^V=f(Q@fc z{qUpy&9^kq8VOeiLk2LF=ICVieXR6n{V6D~zr8ncVZQW!IBPPeoQ(PyW#b0hdy$K)7kod#U&7DI)zVSWux`RC{)(o(~;XKYJ2ja?b z0NS*PB6a z>zg~?Zy+#^(Am9BmbTO1t~fgaa1~(4ST}X%nu68J&e4$Pa z^P(aR%!j4XjZA{ZaRm0CH3HG=42A{=!zzigP3!P|{oDYi>e1^++zwhD-(m7?O@%Lm zX1&`v-NWjE0ZZvSzqD>^^dgHEQ}xCQC&;-~7sFKqLo^r2o-#ubb?F4d6L}z_u-JD) zd$_x@uW0+%FntA zHxnnScOHm^f@aE^SvHl~R;>CKw{~O8=u)+5R0F{SI}<8o5?PZNGo29adW5}+Xf~ZN z8O7Na0~%;hV-iz6BKxFl=hB{Q0SWJ&n!7jPKp43YQGK*Cd$}d=mp!vSBkR)0MfBS{ zt8a$d#AVajD{*if;c~;OZvS22x)v-bmT$!&36!E_qv`?euqC}1Wx{_cIDnsGB10rj zPEJz554&ludE+<}^6GkKDvi`?pS|t#r^FsR{UUrGXZi|owL=hbYtMS&MPCW^xIx-j z#Z6{es+%6Q#w3*^(x<0+E16Y5G`4e=`HN}auSDxRHGycNSoa4!cBGBN6qT8|D)iet zxW_h=1?O60gePJqgd#f3@LyA$g7Z|C;@90wDUIUMWVeArZ09md`8I5=MyVWS}c?{eaL9Pp4LWS%e8fVw@;%TY}Iu3 z@l1vw>NdOx`uS~B!tDidi=Wp&Adx$YvFaA!cefFy1Pk9 zl7=|%u@)A+ZdOgv!b}a9dY9Ix&VE&y~maHV-LdRn{nNIJb*p{n76PtP|`c4~po zEWH+0=WmjbZ$m95&_VyXAyDSbNGv>ZG+XSmp|jN-8jI@)zoR^10}hovf~^RB_ew>J z{Ju$P=YMJK7YHkABW~WXyKW>B&ip{{znr>k$r*bz{q`n1NsledWe=c43FgZ+qFELc zilZe>2WCF~!X^KXQ)6PJSz0S+vedtQojGv#k`+xeg=<)n!gDYVBD1&iE8dJ@+*|(H4EOd5;qeC># zJWe!vE`^(;SM}mM@7GE&h2upKj`Jl1IRBGAQmMZ69>H$RoaM09_I#b?YXCZtouga! z-CR*^meHWB?E>E4_QX=%sJP63KV)+5EC_JXYO4$bYtKxq`)@M`E1Pr%W_YzL_jNml zQ^f%p`JIR>H8v_Pr6m5ux@0D=5<-e|uA5)gI}CjDAprp7E_u{ap|;+|nvTfnUzuvU z;k>8MuQQPFAg?sr?KMlIORQGvzMGFX-z>(`zT{ao7=e)Q)3dNkNO~*e3{Kaf+wInQ zDvKrMY@9b77+q7mdB9;a7>Y%u9Cj}J!7*!>y~Mu<^Ox5;^`^4v^Ly?s6Rmq81b#J2 zN~s2E*jsc7gS9o8q$^hU&ZLyEM7Xf33ALVut7CsB;(7!1*(pK>qA4J_YvkDL=?Wia4N7M`^@hA=(;eA4=qPF}fx#rT8jW2({D zD5^@)Z|i;)vwQkSI_S;-4XUY&oMpxCMb-T(Z_bY*hJ2PZ{ZF2^1LQ)_AqaDcpd6%} zP-J|UkQd_tJ@$aTY7?2~MLxek*3J>)uS|6Lec-F}$6qSM^x)0*L(?S6f~Rfk=p5}$ z?pXB$4L0?g1~V(5CjHaX*6TZV{Odt5=za9}N<~4{rE_Su%5!PE5Rs~xg&!MIWmV^e z+@Teum*wP;V`?rAzMu9vM)1Av z%ey$3Z@e%@8iY-pzf24pux$a7?|iLb34FqrqQ8^F*3?o52f|Iu*VH#87l&QTee={5 zc#%(whNdI;caY>0IiRJ3+!DPIK}B61Ol{r$Dp zj3i&=F%h~I@OP21w0+BW&p+RS);jQI-3;2@E$as>)v;fwKZuA_-28UWb#kICO~v}M zk$M1Q7oRftV8E(hcG;gJEDGRXaPo`|AZ&zb-ti!d4pVCN(uuP7>b_yag<^MSp4@5F z{X<#m{qxoE@yp82Rl>_@(4NWj%VFSSdr*LCERGb(F53b}*^M9wV&M6^ZF1=o1}jpr zdbmN4!V)x*`3)JIObel1M5HPD;lo9DH)^F>2i8zHBK;&~f+ZK3(f}UdO5wKlSsvbM z^&CrMZVVIUSvV0XXo>`tsNdV)PovPI;i?znfHO0*G}-q07E>WWU==%e7!Vtq{@3rj z5Sv-!qXHe84Q6Rh8Cp__a1J_T%k1}xq^J_j8tM1}of`Lg3g;F#bh)#KvI&%SG=P>} z4DK%^s)vFj-XdE$#=q~_E=|7D!HbsB?g;=wpfiU)*=60`a;b|a*v~mQdm?laOzNMO z=i`l&(#_MCJ05cJ7o}IN7~0TtrtwTVMIv2!lCEi^Wy%#R8x-&?rI4HVqQ4bVY$|I%Ch<@hp@a+Efu+}CJAA* z`ceQb8HzW0X|av)V$$1pn3f!)m1I65OZk#Yc0n6rswe{pr!GsKTEp4XtEjqan3U_0 zFZ%80^F^V_4vo6NYWFb<+9bY0ty@Pn$|0yC1ly0izN#Y@z<6q!3QoPRZ4>qYs=$XG z4vhc8!DFwyMNY_6?Il6te|N2~do96`1oJ=AEbsX=1Ds zP&uLDdNC{NocjQ>t8NOFx^Z<;ztck3?_-nKTInlIMo! zQ4HjVsR=QB->(mowKXqlf|_2F=9f{xy#=VXQtYRpP$DPTV22hJEJ&(vP9hy%t%9D0 zcp^yy#>HX64tUF3STp?ob-?$}ESJ!}WlJ5VYZKe7A4QgabsLw|-g6dd?rn9MgDR-T zh<4@J6lRI+gf8Nw({w<%TwV<20w`J0o6kuqHat_I2OjC>AwK1ns$CAg&<5-2 zX#hjy%u7;NGg0Pny~#P%{AZs?7d~0={<7~~^tU{)BVs&Qr^NtaB++6(0M;!vY!}-V zZ|I0%qOT;LW``L}xYNzi!iy$Ns?0L*v?v}CBvL9M80o@!kx58qF*uUCB-NZkLt&4D zOw|C4Rl9cK;RLg002(Z#V9MWo5%pWRhLUgfRlQTM8R#LBAW^wVu`&|-$q!4#&={sn z8Y%`2^8g072Y~op>)w?T1d?q*02d1@#QFGoCAqASF76UnPosWK2%rtSu#s}&?>g7b zSY9zqa7EI0^{>D4=dK9-W;6NcPC<#oDxMoICWr**RG}>?mZ7#;C(xD5QY1 z`90!{wiIJlX##5|0cM+Lqpr(Mq^CUM!6nEt(Js&dlHH33=ao_}Rgs~*KrF2f*=$s{ zfd20^LZ>5=w^5|1|C!Z;j3uCO?>WR1;WUtp(2#@araXh`qU?pZ0b@9be)(_|dw}1k zSp%`o3pB+75DqhBIy~{nPsc^1!D_*X$pCn>_)Pgw0Cyx}eAnp#4jn-5mtaK#ZDKea zH!6Yz&e9D9XUZQX97G8~qvM1P86cKxqfL{}3?6Emm>7O2&P|PrJOWHKeV?unZquIV zwLtKe_)32J1B7AzJ`n^4WQHIDk?>;Sab|x(9(9@Sdu(e_=|g|ltOLSpP?PUkVKTuN z(qQI8q?t)B=AJcyG^|-ai(b0AdVRURqTYL_am7LEgRAk^p+`1@W!a~Lq|BY zbhP=BZ1QkwIwX+|n&+rMI0XD+M0A>%UIuxu7Ys&&9z=0**i_lk+e_ty{U1UW7AO4M zDSoem%1i9+Mocv~(hO<}uL-0NfAoJ!`TO=O=Izf9O@;RMdQrb8_aHaFprE&htG#3Y zPxgrW0sa}Jrq4!B9k{quH*6Zx1-xWw9o}{C^=9w(J-1Y%4E&lj9{j3Dxf1H8LYeH8 z4bv+3HI``oe(>;bWT&|8@Tm91ng42)EibO(t7vlqA0p4~j(>P7o)>%Nd1MQ%MqXL1 zC*P9``^a5h#aNkzEG^ce()>6QeT?F*!E+QLG&thi!E;F|GCi$i_i2wtR9SYn6l(-6 zz}L28KLy_5UZc0b+XORcy=BIZFDFA%{r&hG<65%b!;19#B9`2IwQag%9a5R-;`QP~ zDw>S7MYSK4Y?Joy$fD*!zvyJ*Y^=Y@T%>(rkH;bgF4q7D?Quc-oQ$Lf)#$zYYVY~o zw|K2JSx_8FzEM>RXc4JarNS@i^0D49>|KMiHDOK5v{S(f(+{v@ObDy*B z;zi#sN*#7QcOJy!^55dt6)%l)oKbi0f0sUfHju*9dZC;axsT&_!>km5(_O29+x{SE zL=&XC31-r&N>BT6+{<3^*e^z*Korwcw7F(>2@!Oj&gajKiMcW-NwjG5bg#&gr52_X zy*fP&^a$FWQ<|%4=%T&)Z;f)v>HLUkp2ct$2;V2Ega?u+WCST+qk+sSDJ##_mVtuh zpYvFX*l_%eU%>#$BAf_|3(nqT;8oFE`%LEeKT7luSOBR|4%9vtB?`%36d_0&8J>Yj zMmc=q?@);)zup&$r(=4*hpLehNi50J05}QXl$uoBaO4XqG8_MUJAJ}i!}g`af{76H zOINiB-)jZ{Z~2ttrD06tKI(p1_BoKyZ;o zEHx&D*Y+ z-{ED@cJJTIe-8;ScfD6Z|DNjK6^Mz=3t$ZT`+|yS7XQPqy`6dmap%oYcrC^dKZE$G zZoD_^2B1vNS>Do0L7@dE%Orn{nWk))S8=Q|<-^0V{9;wYh z4?cOfC<40=IN|N*W|GLR+R;tKsnBsZ6)vJl0;9u3S?F})(Og8n&TaDWJ{;U9hA*Zn zBiMgxfHHi*cT@$m-jS7BqsF}x6>{4f`-SbO;+mrj5rJT zo?4BKi@%6Wv$_~jGntZ*n{T;S%@|+r@~t(87mDw{uUsk~GApKaJ#?Qo^3M2<+%K+U z45c@VHaEDV?!PBa*)Sb$#-Wa|gjXo=f$gG2X8$9ONJE-cTei(eV8D^R(S)o=m_zaK zMxQ2zcQ)9kfvrgty|O>L3t;B**H2;H*ZukiU(cc1B!yDPMyIP7^eKBR;BSvw)0i#( zrck=sYSuS88-knss%xhY(-kLmB2Ku|_FHt|+VozF%h*18!R!oX1a>bLTV0Ri^=0BHtV%K0yyY9?k!GD72oBv`f=D zf!xMzSfp8|?5hB^BMX*f&hsO(HB@9))liBbB6?%z2!A!z(6o<16?nqN5oht@I1Ec` zjU2QBvE$UYbh-|>jgk)A%E(Mi_b#uR57;Hu#xe1v2wt?X?|?KoP<}*L4zdX zesWc_KI_%mSS`GEY1>dP7`R-{bdPa1zI78u^^itPK2$TC=phPOhKT+`rfBd_~ z4 zew5ipDo;N_F^=X~{&F6a^o{QBMW3w*SXim*;v?;p#wkbRbZuH4eH z+SwiPyvgm9^_HpyqZNj4REku3+5oaYwLZfmu~#okuTr@UDBc;J0-h{GKC3d=w_H%T&FZ_N0TKemNBWM@~mGgUX5f!{y&H@e=? zZo;%D=85(zS0CL-Ud^ z1eskBOeuB(Q{!>L?@c@{KT}4h1sdK8AI9%&JNgq}v;X>?{AP9(p&{rFRm{F)n;ECq zL#yGv>B#<4rK$^T~LqRT30jrh8Vxy{y(4I_-yxFYumtSvT1b@{&ceh zt2$zd-^81A*+QBv9$)7g4_cv?ibpP32aMK9=oykC@!IPf3jnV8V)J+6bey ztsMmFu&aO}9EhQ6yG%qNRK)n*Ct4ivFaQjihKA6hA)~4v2>v1g0TJV$8uS=z-y@Aa zskz1h@UHq`zuG0sD-3aB(?Z~);^XDvO>YrhlQ99bh;SvYye?k<)0B{kV~O#wLLiA2 zh2QW#_R!260L3?1s}B9QO}u4oy9Cdm*Pb_T&j%=+3p)+6oW*BOkuVWF24HHGq&Sru z<2Dxq++>IXV2m2`|3KBM)gmVsRc;q}wfljFeg1Qdba>Qmho8G2|G$m5ncl0j;hTcD z`hPh-$s~hLfyY#^g&1RLM+_rfG7rYp5KOqkcy;W)zWvi&Xq)QN$+Mfmg~BWOAm2W( zsTTQaO=~6l!)cd;sw%dJ0$h%(;&<`pn2sOW7E7e>bh^2<`>S!3mD)9g!wV0CDjX9@ z7$o``(87#t!^mEcv%IL3Ty;x^i}F1HWxiQRZR+m>5N(|!hQF1io*D>6 z@Vv2W8IiwVv(qKue3z>JAt56%+-jxBIL?mHfP+aHCSHT>TyjavnON%@rv zsQ-HSSx@}Dn9GpINL%k%X(9MJCvpH65q47;3iiRyx%$_WUgFZY(qAP` zRTy){J^7`-`#*MzY(LeP&=(_rN0}&xL#kb80HEv1k>twBVV`z%tbd$K>oCr|5F+uV z{P@>FSe$pAiE7`84nCVZ>tt4W`$sjXtROpZ%VX!LMdKX>XxP?~xt$ajmoNhrVfP*O zhc)H`f?e~$oVBwr2hM5=O!T&cU2fu=DkHYF5yLWu=R88ipD`bQjA$6%Q`bEeB)*T~ zZ!_r0v}rp}%vbqsvvS=Pn~ym&T|P(FZo=sNHApZ(mSlfQmz~XZvI1}E*VL{?ODuzA zCqxMT3(i=4FJDGjKDn$0Dk-=qv6D_se1*pKUmTP}b*ko(!O9TfStYPs zkJdLM+6i`5!7nrhx0whO09|#SVxajK~$|~#cXn-Dp5oN@H59lS&+Kkda`2C`z-B%pJ+k2JHZCN9SuUJunE+BO3xYQeocBZ)=_4;SE$hOs zi6A?#Crux=;1(WRoZZwO5QsF|w+|4ux;qe4#&c3J78(cS3{ujAO0W=E(gb5(xJgt6&XPi>0{{8F`(uVXZBG{&n+ccs4NKNi@JSy(jSfape?-fAyye zckU7kyyt#f_%GJ$EoJ*D4z_^CfHVW4=CS_Pf_XHhay;)Y5o~)mX2s2?%6*{vy`N>G z4dSlIT<`Q62^by<$P|3@O6*GbFtq1YXU;yF?cyHwt6jg>^~i#9d#m58Y8^cil~C|( zE{>*pI#kWS`fDzc4vVCH0?A0>q7I{3kU4F44@dj1+^uhVSo8TlRUXH2t<)rZ)_Nnx zRj--I)1T}o?3;hqNgT3W_|}9Y%d~s@baEoZ3iZ9x6x{Nea)f3BsoQkZpu$v$DTcRl4Ek>|o34K3VtJ%TnFcy&)<=0s# z%(VoS>Lp@l{&`tZG;;rGtoU!PM9A!XW8GEd)vnFt6{5H2C|#pxBt4X&cJteQCBv+U znq?&kvlk8i$dF9AJZ>rC&{->L>^^;5UKJBiI3e_lajxYTZsD%&yho3a%i6faWP9OD z32XbdER)|rWbFRq&v;~P!Dv?OVvBWajc;--htICYfkeL9#Ye=Ri{Ds!a=*6Dx+hq^ ztuo1fwkJ#GW37)yW7_Vw&f?!|0+qu^LB0E$+GMn)8?8^A2IkC$@*BG(AJ_O|v|^Wj z?e&jT=qWE|L0b$de<~c?Z4@hPB3pY%`)hzQ_Fw+<@4yAm2vPl{1_Ok)0VH_5oe*32 z;lx=Ws(kI6?=?y$qVK?$b9ekEO=@6Ksu&}HgiajPG6YV7s0l9SpEaZuPBZb*8YHx) z&ez3zWKHUY0zfK5K%OX|8vtqNt)Pb=vz@LI4f4YPH>-2@!(lwHx<^b=Aqg5|IG%w4 zvTWh;7b-NO)uL&?z_@7#7l2)9b?du6$I|nI)=kht3xfUg@2><;XzO^4$Vlq_L`Z0s z3@1dqKRTx`+z#(=THA~SrkCb`VS4ksv+JLJOf3DwiRYS;=#GOL`nc_rq9ND z5uW0fUs&?F5Z#e>Vs)WnfUqN!j$xb{`Hu+szB`pFBzq14pkDAri*UX}#1gl^v0~$E zB!$z61_HqJ{CX+H>cd1bY1cGAE?soideyj+T1HS97-z@krc}*C%7)* zJCWa{rNPBqF%9j^@>}zYBfT7TIU-Q8^7xu;osP$PJFggisq zf}n}yEsc}X9kRiB)ymrKVyJPDCL%Mf6iW;Nc53`+T4{Iu{?9XVjb_y&>7Ss|OG!TV zC*u}xO=QJKoWno(2)7D=+Rz<6L}a}M$S9pq3N2z%$x+-r9Uu9~CL;4_M+U~fU4F@ESj<5|NaR=3?I>W8ye|BJN^}v!WD^70W(-~kRAr=ekw1`AWl#?D zylMUv)W17LmM1`}TSKMWK>I0Yt2e`ifHrhas&&AmW>;rxY6WYz5 zV9=Nd4aZQ@W1*#lJHumtS?~Y=3|j07GX995k>Idf7Dim;;$8I%FKG9}pQ;DYX8+lt z&Hu}`LPKr{5+DGyV6lK`1SDJp?BM2#q_YHQFIUkmHgJRhL@|9jeP}pRI*!clu@YT- zO8$D_qOkI6nq)0PixvhW>sZ>Y46D$XAz?dVMOnpN8Tc_xy%pH^$zT6#z2IqnvX&I( zcG>7>2Vax$9%p?$2i8Osz$H1*J`8cPi4o_U%hstx*?|4ceLjZJ(vTVY$_zv3B&{%eZH_ZBO_ z7EAy+Jkh5@aXWtRd;0e8r|)3C^S1q_^~`oG3#wG}J(qt!M!a`l{?Cv;w}(!Nle=vlQZa;Dts!t!*JhJT*Dlrw74_^c|q z@l<$1;q)O{%0VilCZhE%DaJrDKQEDH||_^RNi}WKB`f55wkgO z-vDDp#52KpP!=SS!$)Yi=;!2CH2VnsURR2%q&k6tCY@df1Cj20(;ZlYAfmPH^G8uR z#?QGZqab+>GOTCHT!uaCQEkX~@dxk$#0A5j*q?B~yWuox>u} zjxRZ6R~9V2;j9qn6olKD9SBL2?|XyA%QB-48g6J!f-n8!in=!*^QMB2}d9-(Y(Am!uI8o_bHmzkJIw9rM8ZeR#{ zGF&wYHzrY&V?0uu=QmRP-S;^=U{ccYVEvaG@4=+y2hTT4FeUtf;OrG+b10-GRCyt3 z{W;`Jp28A7YG`%kr8Yi-gwrL?5}wu(X~B-$awb@c9aoUf0`#ltp?2#yTHF(*>TuE| z+dP>U7!cBcvbo@seKpJQne%D_5^DJ=T{Y$3&ov8It8>eWGSS@KJfIjGU25qrJig0TJuaD)z6;(GkN1vE`E~6J58TTh0~8*#6iA!ih&=b z4$fUSc}b`>K$di`LW3B(o^VyMn(^!O$a91NiK zH5CSg8MwVBc25#Xfi{TL5&XIV-5_9e-4y<$4kU3cUdtZU z&``$}7=|NlAm*euL4WTz z3}A3EMu(VJl=g`x5x0ZbzyEdzGq`RpE~fl?o4Ie|b4FrdD-&4`P^hh^mlrM!gD+cb z?Z7aZoOL=Pc$jOE#5PvPA>ZgB3A}&B4mgsk_a0`>znXIDykB=%TP&%n>pEe&BqIC@ zr6-^jcC(Et=L+iO^e|E_Lpg}ahKnE#QPlLlYP$As#VH!t5eHQ!KY{G}s~ zNCo@XMEk)y`W>B?_VCom(M20EFsh$$Uj2J@4u-l|s%Zy3m`)&O5Aqb&0quYFyjgyn?^Ii%Mdz#=}<(3hU5h{Vim#FZX$telurUE%eTH3O}c0~F7_7`f2z8(@WP*+8zU&>eMP2+HsahZ9x9 zDDpH0ii8Moh=X=$Wys**9{L&*^yYL9$=v-`_l>^glYX6wb%_v5caHMkz~tcs!|quG z!?fY5yXNyaNrCiqH1~816-Rx)yDNDVZ+@HN^cG|b6Yw7z@?1v}mg8dqe)Rz4)N!~+ z@jhM|eI|4{F5$6NC#3{b__0JbG6$S=5hc;c#uG?$7R>%V-|)IUQfEC}ZmV^xV^IvK zW{A$q%Y!vKqio*Wu$EOFw{|f-y>NMX`Io~klt*P7{FyuDF^}RN9?U>p%i$`?g^6&t zSAbQW`dK4vDe1)sTi^a zMRQDM$z=F}x;r}J!G1s!+zIy8*qLII(xsvv9nJiLdQ8hZ-%i@xi$xejEsVUX3S2a7 z{`I9$6T1lgt4SV98wXDqvc$J<@~=$>j42r8h{0VRsT9h%`YOU{24aj9bLTxMrWT3% zG0^D0wVcuu9Jo}|h|>e-oZ}54Lo?)z926N3dTfIzSmYRf0|IV?s>uP3Zau;HaB!HC z8WSu=3P6%a{r*m4>(7?Iz84mHM{UZOcV?+)i@S=eYVp{SNsGQjli_1KaG#(x}xPaB%Z9apf(9U?*`dw~ek283yhz$-~HSyMJU`GzHlw zd9Ez|QvPsbm5BZ6PL@$sFH!7-{ zLAZ@J*3Vzh$O?X6obfOpH(|YNZ6-96TNQQOVkZeaidi@#gS8^PVuh0^fF+Mwyw=Yj zjnuTXqqt0z*_dg?1DoV(pme~MUH>*=R&;>#me=xVkKRWi?BL)1$}Hpl-7NeDIM`}{ z1_xzzC_YTMgFXQH-GeMr{5Lj2T!6iPV+Y`Cc8L^McEjy0L%_7^hixk!3*>sJVmfFp zYak3Zt6*JQ5vB=VSRa=;2UH|lJEG$t^9-4iVseJDl4W_bQTpHMz8*~)yS`e~{p_V? z=(ACMJ@(svbgTdO_h*Y6^`#<$r9b+BoFxv<-2*-~7YM@?b-o!&eTf8D)9SH-lhyW_ zUYm|GF;Rakdt`YfeV3IK?o(DCA-XsvF3@PNs>=Q&@?5E=vQ)TC_@Fuutbru^mmhLo zO#~&G)MfRGOAbfCzR!-eEiOCnku|ek;UNGl6MZsb&vn={7L$1pI)t)3S18f|=`(?U%gGd6bz2W3G2qxyf>PXF%fi9WlX4^lmbiT!yM z`@1L>aK98KeHyc%RAZ+{gsEC(4$s@Po#}YK)au1bT2y3DmcgWAx4j6k(d%KJ9&7!J zuog)CY~7^AOSWvu1>_&a9dj>Pjj_cWD6M4Rr?M2Vc{oZ*Mf_L(FHFyoL8ymzi72el zXmnAc$xZD`k79SCtE<-ULX&%@^Rue51vBMGswb@E2S;L7VQXsT0{PY^j9-D*^6S?T z&XQGx*g>ko`Nm#x7MG3aL-zwaG0RI0d1o_Kq}iL7ENx+H{0rW;AJPDWoUeGt)tRgL zOGg|}om!q&9MM124q~HUKYh1hEm29m-AsirHe1D6vYIA$&)snz=Qbh(ZVNAOW7OSC zU~xycnfPE02P|+^LaroN?mA?tqZmzo-PL|Ij(=IH7~W}>QR#t}jvZNw8zWaqsw!}c z!R!GsCnnqjz(%AFl0|ysO&Y!^JCLk3);-%sw6)?oMu;P3ePYf z-F(%sZ9*XC@ZMG^=jjhh0v!E&g3&fG$1ne*X3<+zfv0)xy>1lq<2?08Qn@7b!?W>l zL%S8b^?2h$zHRDjKZ~4r*OTrO(f7!GJEr`+Ofn5Oi!2+XjXkH%SQl`NH?hgg?zG3x zrU~yjNx8%iR`!R~lL8p_#CuY#BTHx1#JcHH{79{wJBQ9%tD}ra(l`#Sme9iqcC;<{ z{k^p=;-V@qYX7-9|Eo0^Y3CTZ8I=C~$vAe(ONmdH6u$VP`Y1(3xB^cb*=4}t@9=+b zIy)Pv2su2>3KalxpO+@~xrDh$jMA-Q4?bm=F`%h1`B*0Y&Ztlfi%pdEo&WoKyy(;w6obtyizt&t}Y`)=2-!0F0y=5waj{wuk zjV_k=WegFGOwnz%rHG>%@OWX2F?o;X4sOkL4uDLhG7nJ`7W;P!X6(T-;*0u9?SZyN zK9odC#)D)lfA+&K0KpA=kYKiWa#Kr_I&wn@qH~s~mR=0hNj~jv0 z4dp|Y3daaHIMpp=X(Nh3uFP^&naQ=kKi>#?S#4C^yvMa`SNt70#w{rB5M7j*Jsgv# zMNY5!+3B;R))Da6&wGEaO@i4e#c8JH*8c)f*BzFbTEC=GGf0Q7YH}>tXgt3IA-KyX z+~@3*qv}FSq%EIT5yCMlhR77+vXir+?86jc5!Qk zKGPl*m1Is42a~vQW&lUgH-#X1tYpWurj)Yu+OW$$`SMP2C~W{j9c7wzdMD`XJIo85 zRJOBEL$t&uq}slth0GDCfIKH|<*!C&(F?+<6oDM}#p+vQOY7>({1NJHFMjaro$mQq zgvVBw&WSl2Q6i+@L!kzpA1wD^2@ycBL3MGk-7_BeGo9VzfG}58-{rhRk(DizB06B9 z-iQq>r^EneR7u z%4a*9PIA-LMFHNu6h?f7T{nYa*})yfplISs(L5-`pUXa4zJ5yXs2IvJD{>UX?4sB( zM8z?#qBfzcop#ThEUCWmGU;ugO_0oZo#j7sVPUR>5fxeVS<+Pp;{PF&ca(Xi^i(cV z9v1nH8XNGpL_=Vcr4|o~#NW2KSJKu>U?i(%aegSVWu6tq?6Iy&0phNRf34_y`apSP z=87dTQUQ|&tah)RHSXWp>cW+LG*eG|aSwL=<3aepS^$7)M^R~zCX>~*&Fv0!iYWv& zV06%y>WhgyFuB|;!b^nKgyLQHjMV^Q|4}ZNJyi<2cC%F{()lKt$c^6G?az zfiGAq`_EFw7JLIsPii1@fa{|joFoC9@=Oc?5*h8tq=|p0Bq(UzihZ z>K59Ki~c>+-H-T#mnh(~v9+^5=!SIkktBoOnzpI&ioh(1Q}OLF7j5ugnU~D^Vm52X znc_o;$A^3`G@F7W>uNDoN33d<1)9y z8M`Ge0OZR=#WWhAP*|ZPNY&P%>vH{z(KwI=1fLE7un6FwQ1)xZ0dV!QGKPnEDOt53 zF}|Fe>p$1>baNKlJV6?`1{O9R?T?JrQP9MPCYJv8&mS|8$!E8gA&Xs(xF1<6s?dI{ zKzIsfkrX~^)!nhwH5}#PS8fnh)!KjXnm#)le*POpkS|K6`J8U)721c+F!|bA8Q#!m zJuk$F^|EKP={!y+Aieed@CX)Yn9a@o_RE3tsUO?F&{(g?y!vM$k#)0MnLRxn-{&~n ziK0Js?;9X}H46KYE2l17F%6uS=hllXIG7C;Cqh{|-O7VyOd%y%2oIfM)6=i+j$Y=I zi!svLpIJ_H5NlaIZJOn^k6aLh^k&cjBs=KalePbiz1P7;cY|sL6dNO_0j8#hAB$8L zO|9X|%U+sTmcv4+$$0Y5SVNWfmN+Rr@E~|*v{K=_!VZOGElH)7dBiBbDzsp<6PG6W zHbf5tY+r|3)+u+cT7IQP9%W&_*HaU*tD6z-y`@B}S>tsXoWxid=U=t$9@ka7u$uT| ziu@*@>8<$jPk)>|hQR<&g+^_naCK4obzzW}M!G~P6dDGF(AcdHRGAqdssB|`*2Ob? z2PouYb&$_9Q%54RZ>zk(#g|J{Q_oqT$)=u%$@K+BZ0-F;9{Gs;$HsE1o=AhryfjXw zUdsPvJA#_*>93eY1de#MHyhvn;FZGpXxil5nh6ZEA<7W$-SCB?#{NU>ugWAeczgy( zSu4s4($JWDIm(V;%>9VNonc=C#|HMy+9evYqv$?aSE3)EF%X|)HkK#tG|OjWxY#?r zv&lI-*cvNWN^8r)^X79^#Ws0!Bk8swhT3|h4fF|I~NAOQ6Yh* zu3`IhU;0*kY&y3ma3m|_$zcQ_rL?PRXGPLOjLsw}#>@#?e7?o$No6U%ru}1P;OQhc znCAItmY^dc)XWtveY+qiAk;1OBONPPJMRC{bdF(_et#D~X=<`<*JRr^C)=*ccAacZ zwr$(CYpThb?0U}ce_hY}^WvQAzR!2>z4lt`Q+rhAGc7-_Bn9=yl2M{JQH9-PQke#Q z6kYVYYQRnnai`RmIjG)?KM3sFOqJM<$@kZ@xRje%y%|d2&@TSXxcIpBLVx0HT9N+4 z74pKdMt9Ul5wrP{yuiJdak9N{Q(APwbnV_;(d?wvh;lMU5AFd)cd`g4Pp6K8x?aKmDbAhcvVPpLdGcJZ9D1rzM#R**qWz=ejLV_E~v+C3&3Xvl(9t zfhGXv`Tlg#eaCe|HU_9Bmuc>F2*C(-ZQJO5TjTw{LctmiPq;nPt_QYZY6WHG%%U}w zSy6#0Zvx=pTY&*~!5MPY$hIZ+)W-bw=k5oj1w_4qkbFdX8*oO!!t? z*ij%nRKZR3OcYq@@ZwnqCAb(wUs4e4$V)*DCoSuGRbD_#ORCys`s}&9O=d|8$#3~F zp5sbu6As>12b%>3^U&rRt-N_JQ}lsC4X(cbsov{GIQkV|m=PHW2aHt<783=4U*gKd zo?sj!YRi}?zK@NPQ3Y-Lkz_k>tQHDn`~AV;l$jt%H~{z53;-`ws{RdB#v}lwi+h?| z3_v;frV%18mAgz&14bOccF6#P@%T^hZ3oN$FB(zL!-|f(yU(OC%0vS?9Ud4uF$)xC zZ1z(QOv&OYT+g{@i@;kLXV(t4D$XX)v^fAEb$MA>@flC|BwH%J^yP(VCo>}$tr%;4 zi^Zi$^NCXd)Z@P?1LeTH#MG9F*=b5FYJgH2ij8lIS6z3Au90E}KrhKEdJ;%$s~Tx6 z0y)!YEjr9saTxm|!hv+%+LIZj@MvU*5&bN87nToJL>@8#;O3T6GVBU(>YghNeK7BUzi!)&+weYX^r-gJ#4VtdU8Xkirrb9Z=;?w&rzZ5wvzpp4Cy?vKY!61AMIRtF*g zzJSk~HgC2<_Y;<$3ktyJzK}@$w8&yoG`FxoGd74~3~j{Gr=C=Ld!4U4*sjju*Rn;p zTg?<($y3941_FT69W{i4z?~GH}o6>Jl=)_EsT^ba5Kx=8;vRF_B|MveR-KyIbK9$EFn z@JpQHV9aP>ur_)}kP#kA`;`-QG%B@C&PN=4&RrnJI9-L>jzdo$g*jhNGH6eq;^LmC zWLT@}jeF|y>u^03dq5@zzayq}!q)0_&Gx)D( zf|rvM38V@f2rj8*(u^qcYPdp!_9id102^4ZaH* zOBZ?{GKYcYi(ioM@GAR&y zb1d|;N=_*Xv^t)uP%r2dxfsc8j%(}OemgA@$-73Mm9U7*C{FYob7m)%e?<|H3 zIL{7duPMJIx&QCaS+(aE2#X&BFgaqin91UCJ&XKN;6qzR5P0pV^}9Sba6WC~@;%<-DQt=E~Ozmv)%^)HEY@~LZL54_! zLE5+0^lR3u5;uMciPt$^X7I-D#9D4{dwq=rZZx_BqeV2;%=c`Li|az~5Ww?-BbY^Y zTIu1GmA#n6&AN1^HlY$I*mm-nG5Wg7-6PC8+P|^}SM~({&9$h22V<)S9cO#h8#xsQ zjBq*|s3l3O&K6s>w6vUgt)e1JS;agN#&-n}Dl8(P1M+;Vrs)4ulo-%$9f-1YOFK88 zo1(cjkWo6QX(>s`)y4UY`~Mh!8cuP|g9mMB8_7z~>;Xu~NiSv=mgL)?^j$^x^EJdG z2fhu(ual6Ou_rvM`yWIV(a{s)^?Rv`g_qkmoh)T67k18MqH^)Q` zJ`zUix*+2JDBr!GR@9qv-+eYPh-yJbnMtWBCjD)fXEfzQ^qDgn|GNxXR>z{^ykyf} zx=OEnpI?n5sG)V${lJ7NH7UP03?e+uEfqaiZe5Cb-rXe8Tc*j;)*Q=cbn1d1aFSpN zJjseG-4HRLkzubXdmno4_Xu!~l=w-wCqjH_QbfxNQ5^F`-A?|N&hD6egoU_%f~2fh zoSB{W)DYEmfF)G-wwb1Wtv_~hkw4BT+4W^3QI8@j>VIXLWcf@l-E5D~X|!eFx7isd zDg~`z>M6EZC#ePL!oyf6dkZIG9v!M83~ACd0XZMPG`^c9GuSF}1^k{!M4e@uk5p5j z-k~N=82jC^96!$y)%8O8{uEO@f0+_1N*tFg>l4A{Ts-H@;-CXZ#`DAqpWEa_ke62g zf({9CCii8);%AuKI}Psb0yOdQo)g}R(o&HsokC3Vty9Wq|2~D)EE?@;9{l7{^y7~~ z*6FWBu!Y7piFUTa*^?=GEf@f@q$TP)O(xCY&K{w}ETiGjlRcGQZLqiE>Wo=S|G&0t zzhHjm#Qw1;>Ex%1=$dGb!gINy96XjyzN!6Lta^rQ60|oYYuyC!i1NK_OL?p;7 z`)JNw!uyBjmGO!Jj~Fq3{@^=hl@8W(BCP&zsB|Ge0j7LF$rtyo0|L@pz1;^r$yj{P zox2=KG0<4KD_1QQE3(@hH$`>#(qat)h`^Aj!SDk>GTvBWUjN3+`2Aef;R*_PP;Te? zctkzBljzY@!8*$u68?t!^kFfu_cDoU2$%dF+TgA2obc1o>P*o0`0a|vX@exN_;h4s zkkIFFkjQ13B2B;P-&M;dh~TH%dYQv*m1W^XU+@Dq{x(0(XUM$W`*G;T`Ybi^e5O`-PV>B7B2$k@Q4eK=Y=TYbD5-WW3zIO zfZxT;=5^c0Vy;uCMax7?#&$(JUz?Vl0vyB^EdQTC4ZOx-_)&%I&bOg<+_B`t0p8se zB+s#6qK`32J-1VFCSLcqQi0Ee-p>=Cdevl@BxB_$AbJVy^OUUehAsd!Dc(?e=mt`rhu$Zo3nqKRU(rTy>n+{NUSmJ}&*b z<#+f|P90g@9vu9Jmhp*iVy%Tq$~&v#x(Y9-0|OX zF!>EL99IE#GO0qd@a?*-vw5YU-&zG(8FWp)w)OUN7JheQgNPj(;g-kyv1&z{a|B=D zA|tjR%5Drv8vh$vN%WZagUIm)-FMUHPp!cHtHH~1 zBbA~@XW%z!>TMl7RiR#3WXf?IQH|g&|UX z3~n9?=^S;EurOY-xZ<70&UI>&s#9joPE@Ktovav6Rk8tnfdFu61>|A)pV$Wr{)pwY z{c?h^gWTz{9w!wgiHErE+!>Sao{6#D|XEC+vV|;p5%Oy2P8(Dh})zncn$x z^vWv92DWSyw$L&qHK+OP??W;_C{8`N)3B3M4ZtONykYkg?2^S%b-b?lk^9NjUV`6kK&{Zf=-YSFFxcy;{Oez>!exokH?! zxH5`eiP4ciID{OTRfsgxuAOxkX`gn#C47%%2A?B&##NfgL`zhKb{7oxS(!z+b=+Lp z)#_Tr#9S9_Rth^5mnc$sjvD=jQ`mHP7DKC7fH*6cvN4K|2ty)Am|5 z&NzwjX45SZ#IzqiPQ0|Fj_$(m8Yi zg&&qDq+ks*`O9Qw^EJ#!&&`Mi6>Y1iS74+av)jI7zqMKsItU1Bf}$+)Q)WX8rQbI7 zKd*%5;4xgs`pQR3^J9+pmFF?#_XCDa4FCi&1PVa5`)-8u z{Z_vBW7i!|5PGw~EX(KE-T%5(@Xjgsbx2bGb3w!JtJmXnG3O;F3;`QeTa=v@zNJy#DO*e*}$&>NE|2yiF81vJfT<@5AwR$HTGz&4Wecr2gZg zp!>%I(vC&|S8x5bu;4?*Y5-jTsL8Ll=c@O;b(f)Xkk^>-t%&MlE~@vdBb*-wM+VXs zmv$|C+jsl?^X~eyKl5z8YoqI8slA2ng*mR{3QPY(#^ezCa~3O?yZz%f?W2-P@YL(r zjh?-h=k+ws_wl43TK8^3G);{J7^*n6w^b@7o}CCMSAAO5|Km)yNO1SC5cDA&P6 zlS=?P8hLn^1#N#U!q9*er4I&Jv7rK}&}F3{Qtf}zz&Y)6UkkQ4FDL-I_+B?v!%nN! zbIQslH9Qdbgis*gC|3t}aW*F=_9*ua7}_T}6~jzA!wqs5fVyD1ZYDrG`&ZVS$MjMD zPZZFvWsYm_vBRX+RXcQB#P2-HnG-zN zYy>P8S-a@G2k-ULX!u3f>v13b_pKPNtJpYjcY@&}CWJIM)7b)*MynTs@SmBd2Q zR-Nd`x!m6vWA83=2IrlpN)-`txY&m0TX^w%)cJ8+GE#YE4Wz{lyh=qmBOr-X=q{yl zL#0BN$FQ;b|ETIdlESU6h6gL7Gh?5SUH7NA$!1d<1#13$J}vBSf=86*CqGRCPgVWK zoKA*<(wwE?L#gm0m3tRyEJyr-On%%{d>zN&Ofx%ZnEW;!Q&71^Lm>cz;_txsc zI(;Kk+VaG=QT|+8^=5L5K)RnO^co=GAUa(&uZ2rEK zg)fq$fv=;VH)>r6)I?{KYxyU=$-ZZyJ&=vA%dmKeo4Ge~%O8=jsMoTeLdo&OMaIQ@o~{bnizc`rA;haSvW{LkRO ztG8a5p@~629@yv;XeFa>-7>EBopH`_K1|UNcw3)kn7n=RyM9v0;p3(wN(@SHde zcsjeC_y0=kpzllu6a9J+e}gl0 z;8b`phZNzAuc7{LtRTK`y+~c7WmJPBcv8%`I8x71>E}ZXqvtr1{}p6|W=rPoT@cpi z;EBXIA_vs> z^~KuK`*B{QnyQKOZ1_<{rm?#f*yZ033(su55!ta`*n-34uh=hkg7CI9cgrBt8 z0=^sG{ugV^r^fZ3U!Sl55q@9$&EK7giBP0vr%Y0UB4R91tcCMiaJ8NprOIoR{x{`~#^TZxy!9)V*Q-Jjc@oH^Qou#r; zxVl(`sS1=yDtK=a9ZYlETXMeMsAmQiQ?&#lVk_#A1Fzr&IV2MzfFWXJ68A@~=5Ikj zA;$nPfXg~-y0leHTilBrpzi`8WU5sV8IPO*6`#n?hM$p71O<7aXoQT4a;0Ea_)HxS zCU!!7bP$aUiY9}J34;@(n4&?*Xa?4^u}G206u?UpZaq6lkw%P9OEkI)6gKCj z&|Hvd;H_+NI?7PCMHyB|*pIFC|NY97kq< zY2Nu!=m|jrOrl@Yiw%H^K@SG4o%0%J-5Q=N2>i;?IT7^dij-Q1p=q9eQ}E#WU3GbM zSah-yl1wF7E;^R8MRyeVp+S>YAyyKGN-FmNsq4^ecfIN{fcCeaUlMy$Q)NFL&3=-C znSvoRhXEqsaiW;l6-wbq6Hk>f0O|Zu&|qS0n9}iD>AZWp$|HHWTg<@5`2783>2xjD z%Y}NFYg{qY-YMg~zjbAOg5Uu`lXM7|2moqh7%74;WI$MO7%OuzMqOLv7QS0D0cJ-U1TfWETbelLQes)MQs#5#x*iAdYM< zD%FdmOmu_a)iWtWufQ*sQOcq@-IY?ghX>$8;tU zJLWs))-dpWXw3bvz5dADeLn*A>)UQ4<%M^T%o+U`!ZhG7`4L&Y+Ao zji%2ien2c1*Zs4yQqpyA;DaPbmHXDY>31;Q>wIh>C@NOk;(koON~O?s2-=9xBCUVj zr0!>o$4|4oZ`&-jK{rC{@{p|t?@fi&3*pMxpn_pLOb0rn|q6}+hCB#?fDe< zK8Ex)i{xLnzNP`Ee>W$|cX{4>6MXGedw00|{^H$pm!VkA;6La8GVK55inQ@N`}HEO z=l;q#v5aHLh>Sc~yBAB(Wz z(AoYumiO}!lwe0r*c-<^~qSx+uQ|rE*gaDod z=y)%?t_u6UrWyDiWcq!jUb>wNuL^#od>v8soGpmDj0s$OB57uId|-U-v0ra zTqNZ@H{`yI%a-UsgLDv1!B3md+|1o+xoRWH9UuvlwB^`q*2{m9w{LDL=_BsEB1rqf z0g%dTtfUy0{6SEV!lp>XA~4wmRQ3n!-Wdv1`qluY%ybgE$%VI{b`}@>ytMLG(G*NU zhI8P=-#w82;LHVBut}u%De+;m+Lp{?SN?9a-v$$MHZGcscLV~#h46!7AOTR(FtBx( zRk)_nH#3)Pyu7?q3v5c>=VfeL#0lbV`r7w|^MEZ%6;C#h4=&=Q)4P>s<-bNC)YAD=3 z7~I_4#KXgrUlj9IH&Eltb_NkXDroi6)fX*wN?WvDcTUzhaKz35p}!bV_k+m7e)|sW zMdO>VG~i|LV6fP-RK&)L;|J}1+{fO>Jk+o$*%p~omgE+|SCeuF37ApKdX z{cJ3(ZA4jkj2x(zvJD}zDSh3C?G%P&VK)LjBb!u(GA_dw5bGeIUSB(ugH^B$@XG27 zz)T{H>EZIai{jenrx3(x=0YO@(p@PGFwH~e-7&By4RbU`1-wiU==gAAtKS*p0uS`r z#OX9OnM#KA08TK8@a@UOPDLs@IbKx{7ijPg)6Gp-D8s6w>PXVO>^zJGwu-~UN;!~n zsok+iz_E&7qI;srC6Ys!9X{Enru(!I*35BYn7Ip-4Ij6AQ&G}q$m4OVgpomV0!8m+ zzTq-_ew5h}q64u5gmDiy`Olm^q!EW{BFr^Shw5};zz8$P>+9HE904ZXc|##Kcu9sQ zf9=!0nT=?Opnja(TR9F*)OVb6+h`&!@qco*=8K-x?_{5art|s;rXstg3++t2*d<#u zKc@r+t&vt1?28te^u1>bmr_lk8!D#nNi!K)9GOSBv9;7l+#I?1i+dA*1wv1#`Tc}5 zbH{-fr6i{CXvYdIZPEW!o_?O;V3o6raxr_layvO4M$lOlM~PofUof(mOv9ZrJ( z0+IOkzALt!Ra7Dp3A{g#3VsF?z5fXpeBbLHQiz5_0fz*{%0!|d=cU8uv&75;y$YA1 zqc4y%0DS|HJ6|iie#8E+hpcc^xWW{eb|gJ7!Mk626|=S&D<0Ok9S(xdM0JSCWx< zZu`bfe=W36pMIVON=Gnl@_XMtXRiS7qJk-=i6`}7{1 z6l39k&g$^8*sI@WzEej-Nld(N`@QvJ_|4$OLEq`==7pP^csRMJI7VKq%BGJV5cYM$ z>)+f{gx%;*K zwW$i&Gt$sgz}< zZ5YkG1q=~6K{0ecZi30>b&hQ3?(~(&<90oR1n`|z68<0^+SjBsUtnpyE5X>|Ym3m7 zD?t;6K%o2GvZJ?@gG9td7Zf!t0*Rpgm=`#0TFfafRKgSB*_|O+b-l0qt zsC4oT0FZ|hI&-n&2Wkd`?{J%om{ZQ$ZDM|l5oQ_jHvDET8M1SX=>`?^F3(=+az{`b z4yIK#6q zI*+&a`)4b-D0ucBpo{f{Q+{_w_9uRqXXYC-rP&?B`N2+blpv?r~DgC63G6Kx1Z1HYG>HMe3D++fsw ztOrko)`jd|?a_g29uu$j;594)nw(SY?;cCG4)7&d^xqd|OJoeh#HM?U^}iVoQ<*6} zG97{%6?H>Rpu>q|WJexA4Q$OTHyO8$09G4z07V)tIi_Hg zx&G2EC!@k0lsz*Bu~SL%P<3A%4m z*)vMDz#fEP-PvuV+j7Qi^Zf;Eta#V2{0tk{p2VQvXrb`CcvzJO3^ul^}K>%F<$L#v$s4BfQLX*U3(v#8~ES%EYjP5myMWf zu=plXMhgfzTDsBMebF~M$Ib(fbKIm7(C@TJ9vQ!R6SzP2CrpLgEBP3raOuT!Vl=XX@ca6OM}$!R${i zXpcy+MFYj~!*P{hGjif-`A$FD_C~}KMwI;h>ov)bWar@HhK-d|izSrlG2v|Nv{Kd8 zr_CEdSd?{Jb2*f3BAv`kU|VA&YqkQ3CLW20WCyw*6Y7L-vikSEdlZ^ zUB9iGa{+X+?BTCZfzcjr!_hpsae28d_LS2iLn&1+ST+F;h?< zY_{|BztBEf9|ahZOU^x$GBJ}{*3qyKQ>}-R*~W0#skH$f&F_f zz(u}BO<|F*c^d8^BWhcc=Oa@f4XGmOV3{ye2fako2lhb}G+;f+uXc*(Y7Vev;K${N(t z{3@~uEE|hetA)APz7z+S7gP`a;Z>tB|r) z260~Kg|(o}JTNYEHH08b#uvT|hsW>OQ|0}uk}YUXLfqWjrd$(RX^s7apR9*F$_+(G zrrXg^6j@JmrOq>vgPye80?4dV{?pWyq&vJQ($9sXBH8DNV8-%G@-FzI+!;I+6!I}rumV&P-v}+5{Jb2s^%th=`NwI= z!;{rGU=|90Km_aoCltsLQlshiZ2vdl1RRv2A|-~bf%L3IT#kQ0g6t~-=a%ETSsDSq z*Zrk~|Ci>UDq2L|>}HoF7@BIx6n3ZZbHCg6B$UeW*T;uLKL!`TuPs3Rc8 zjr&F^Aw>8ZAMOVdkvqcmUH|UOG6YXYO}X6NPBO9r4Er>VkHpW*iG%gJhK~i@)x;_) zevK^IHTr+;p;@#Z6Sky-1*~IGA1iG8E@tjLx21jk=^xJz7krH9eJ^SCyB+ku)9Jm* zyl#KU4jaZi>1$m3d9r1%r_cfmc>^S+X0bWayxdJ!VC=p91qbNicUxt)wNqa6YU%uP zWY?Lx=JtHdQN5ES*nrM_tLeB%RP=i!SGy3U*>t9gCHlCh;!mc>%uH&~pOUl6$VkBb zW;AN&{Cclt!$F_AGucF&ku3_axoUrNzR+e#0=?WO`D^Jp0?qOO!@2g-4;n|R-X#@2gxr+(;O3*_{jHC2T%#k5C_gB@bFP{zi<{avp*Swh-hsKn%bZG8qfA9w$Ue zpo@$bu9T*)hNvSpl|OB_&tff0NtSGx$yfo~6RRajp=gsUs1!8Mo(OGLWUXRPf_ZAOr}};Cq+3 ztGn8Kc zS#zG&^_K#v>#Snjw4euH%w@*T&TjhnF3$h`dgnH^HH+I>qMK#}Td$(AK`ld~TE9RO zR*S5hed!0!FSUG7{|m0E9=iBaP{W8(o;Q+u!~C=`8+0FwY0sijSO&$MhWw&3VJz4E z!#}m}k%tgvpT#9(vhOc|0QCpkHwUetvHx*kyBC!NEB!Lhi&C7pEtsPe0>W zA!odO1Ci?~grsEtnJ~2hF^3i2(1M^3{>32LUzl^s*(mLY=B}d;`dE&^GMmo0<=6V5(7Obw7ROle>2%qVbIBX z8mE402uCI_Nj24c6b)%v3VvRGONKtQQQ0r`m1Yd1EWJ_wUJHNs&NHd$?^w?`u4+F8 zM4lf@CokNjRYo&OBKzdY7FQZPFG^aG{XM$c`2q!=K-GCf6Km)0C*&}!ML3!$k|%c! ziKzSAOzSP}k>^NwL_wUSCGs;}lYCL%@Jn)bYeV!*W1B#F00E{MitOgqe0^V?krB^UxC95WBac zYXAH~&qc0-@(0yn0yfH^XXLgRFw+qd&)H0+D(hTk+nD+TSHtt8%1?`8>eoyGG@y^~ zQuMm7LN6EDGT=ObMKotzTdLHPlH)0z%$Jn0^>F>iU`ZGnLXwyW%DfL9d@RC6nyM_L z$f`>hn*l7c^bmNTPfsREt&&QD3X&7|HGBfB2C2w=(o>KJZg3Uj`J&SQy!CzodN3ju ze>0}dHS|3XRYBknJo~mikcqeLZid$ZlycIsZ$BK0KoeI!nQuS11c+{-5d$0z=n^Es zDd``<0n~CAU_$Zn4{XY}QuKX4jX+*D)k|YUuJQ?lV_XwjNY=aOx%1EdxZUSM#kSX9 z>DNN)U>n@7%l#oZn=OF)71BgWaM1xkU(~QMN9NvM)Vi}UQ|9c^Od%LD+(lLY#55!6 zOH^y`A(aAw_qeRz-wJ4=Z`3t@7ptFtH3VO`>Sy0KZ2WF6IeQU4j?7)9xA#~2zv8-E zky_}>&mjG6%&xj;+n;)#D)ceXiHXJgl?H&1=%GO{g_i3xV&QIh;0kzlm4VD_g=BU^ zC-;U-+dM9ti~BHcYew(~WhPR*BEXhgb|qMAI8>ELw-jw5-@PAymMfpGI-jx{pJsGz zJ~h3MXS$!bTlHNlrSA2VZ_c)ye;w<$|2mwQdhl%F&5F{7>eSYGxR|i^uO|oU^LM?k zPZs|m(yM)X#5_NctUS9Q8_=+B`ZsJ`dpjeGCaQ`K#VYiA8cwLI*Bv>Wgn!*C8DoMa z9t?}$3V!s{1Wmgtw(z1K`L<_Iq~}iA&{kcV?`$%M}y8RwLN{^-hGt7-RF+ezcB-c(D#1+9CW;1pY?k@ zH~4zZeJ#)J0KJ&>%^F&%jD9y!|AX_&_WX^y{G%v##E{L0m^yiAWAL_d%zy1k>vFn4 zF)4c&v-=z~>wD$=_2mChSh-wLL0f5-NY>ul>(Ec(Hu@uSUs};?!8zv+zf)0rIn@QD z{q0In9`Uq<%5D?X&hX;QVx^(kePVBGGvgV@}9hTt1t-OP4O07z(j?jb9}9=SWK$ue0vc8+VdgQ~3e( zI(%y$3~5CZkRVc*zhz2MlIW$qBus<{d>=Gjn6{v|K-KA6qm*v3M;Lo{Tew?E4fAJU zRzXUdQi$O8tOCv&EqrA`II~>s5BmJNOZALwRO7+(4NI!lBT;R0PIYpMU8ost{rvqi zeA;zEV@5I=&GCmlPfcj*Q?7r>yr-eJ!^dq=7AK|4j6w30U^$2)uQ;UEiZ*X)XPhY< z^_r66m|pcWkM~RN85aAD`#6!whRG~_-1;}N9jVDNdivcN6V(qm$Gufx?<(Oa25>KN9O~? zI7?Ret@8wa9;QY8Q{`syfOW!AY;w_9Pf|mkla3tRh*@Z;c@u)iBVD6fS)Nz1aK4-d zR?d_mxe?-Bn_Tp<+4-6( z-kJO3j4K&yg93~(S@sz60CC$zqQzSqzhzkEuyMkX*(M!)RuZ;AS~xcAIC&V2Z!Qw6 zUUJq8u6a{4hA;Ss;vTxf9y%c-IeXN2Do8UJH+X9{c$?7Zxh+@pJ%d2v`v-ziSN7iE z^*Ws{6jxMiwCFLUm2X|M4?a#Qn$%9OwKx01+W* z6ub5;{6BZUStRejbCq8g>s=F#S%B>l;tEUoSv)! zl9IGq6T_NiL7`e~XoeY0N=+F(HX~g*RhF18P4+lz`7bg8U=1v!;2){V>JO9-pXV~z zhfro9u&^bT!25OwD<>TX18v^ASy|mqOr4;f7?s9}j#!NRAG{=OhF}Ut^^;J;E)TWF zOuK%jL>+bVVSA%R&%Ku7%hVte!LB>l`pvdC(Pp4#lJi|Ne&{0oKo$7t=;&kB;Y=j+ z=MB-<4dcfG4Bq5V&NR0Yj z&leW_Ng34zo%ihG4(*4jj2#c2J1K&%wSM;yM9%@vS3i38f9St$tnPT7SI_z!v0ndY z@YbkVtFby085~8mWV!R|Zb8oA?Ak*RcSx@{OaPtQJ~RFA76G8zD53`55%b1+CzQ7V zjAV+rF-AOBJ+%3D=i_wGc3l;RXX%!V?(S|2fx8S4?Tci|70<-yc`uy4lq#`yAG@Uj zKnc0)MKn_@+U{n!nymGv%g#zwF$xAynqr9bFXv<+u$iLT zy)LlZRaqQ%)%eo0Wpok$9LDNgF&^ zKTfx%XZ+s4T=Qq+lX5NYd6Gc_Z6Z|iw@2uPcBRV({f8{UrOPq?b8j`f^hK73wHBU* z-0w!?u#BZ%YLtTV66+^bUGJZte$n%EosS+BTI_pee|IZrJSSnm#?|Q~x<1H?BGE&c zOs?~P4_m&93;zq97C=Ku2cVu#H6>aXyF}Z6sKy2pys^rC!ykd4S~@H*EOJ%=IvOm> zs?)zDP3<4wJ{apr?0!kz%2B$oL~uJPq@TC@NX3yWB=xYxK9+aiva=XfR69t&dEDa9 zrfJT$wp6dBl{^f-V_?e>Cvlc&t+9+q3v!dc!ta{SgT7&x$w~x`*BwlE7Q(CjvChBL z(=n#K@2ykEW@0*MvW3%@JaF7mZj;mfnDzs1Rk}O&aPw6ia*-7mt7KaIaS>}`4yoxb z>}oe59RKeHxTg^_h5j@D?YG;wSQ)wDBK1NS@j-VoR9R{10%D3iII9XSROG;MHyAPM z^54^4ocUHni-sN*yu0V|=>nb>$}3GScqN0vVphDO(ay2q3q1~@%2o?KCrQplNwk16 z!V+d?Cn?he(yVH(^OSUx=$(4)CTOQ*sQ^zkW!RM_tTG36L~OPnc_JTHZYE`t{_@A_`}+{o5peHGV2K@tH6yU9CDR{ejqzj#ITj!55CF@J+I2F z!esSQcBzg-;U&Cd(+x%Qu30Vq?;+CvwnB`#H>o|RO$ssyo^?6V|jwi zraPWDbIdj%U+2;Q7-}ITTG(Mhq#au1EfA^aqVhA7iqO#D-#qW`%i!0&fA^AF zoIS8oTr@V#Vu|^zuYlaHJ;0hx0|J?5@83ki6@iCj=0gQtt!ln2WYA^xJO;Uvq*1?J zqvzk?>z8Fvz~amM)%kNp@Fmy(Pm7KN7xkor$Kj8ihn*)jtXQNhcMKW3bl7DckiAZ4 zKN7EmmQcS7KgX$s8q1qgx3vTp#t!Z{RW1te>B*k2v-di%@F8WeFvby}jQ%s8F_VK! z1GB(P%McAjW>Y(Cz-abvcUCCatXlarthLj*m6?D=enGZg?wg7&BSx%3ZN3%yd*DR| zk1Q-aVT#jz(nL7{+Rz?@nQc)EfU*Wr-PrZL-dx06E zKX$y2%T{}0A{mZa3-K48+V#oyO#CU+EIQ#L#x{2zTZmrsiCzl@UqMu-k#It<6DXXOoyKKUJR@_zS}Xz_W{PP$L88@(Ec_lO|>J5739C)h5xfqEZh6m*=r;C zF?0Ps17iA$55Z&5WCZAinMD7L5(t@Nn+Aoy5!@Qr@rj5GdVF3cQhtPohl7@t)?GjE z=o_@$=ON#xZHxByiVE<$bz2p+>F5igBW^qswo(W0PN+PkYkOO5!8dj9FPBcmGcL(# zCL5E6Sx=r$eT5Y87hlHQ5+4Ps#k8kJ*`!2Pp`8fRVRA}@h3$oJnY^|+OigHpv5$XO zt8;4)>8Tn&Y6Ph+og1zPE<;Fk;c{LMI$x*8Gt0Td;Cjsw2YzEzWcM;So&Mx&Mf@A` z{Z%oL+ho>%Vmfe~n&l;@fu-w29dDzPgoEj3a?^ECE3n2Baivx ziosF$OUVnDKa>0@%g*30`pKKy!XtonNHgupK6G}PN#Y$T=--#WI~)v&H>KP;n-|Iv zMMlRQU$*WC+bYY&@4g=6Z&nAnFc`MBWHjx*cIaUnk0@KMpbFnM3zqKHpvY@qSvT6G z9>-n${`~@|7mc2sA533wAx$;;B_fyvO+{1}b3-nhUxLG_ux6{!hsKmK)+EjT@Y1G` z_x>G1thRI2wsZ_@KsS2!Cd-lCjD$w#)Dt|+Gn2fz&g6P5ZJB(?=vE5%|7bd^sJ6PU z3qx_*;_mM5PH~q2!71)m+@)x6cXtTxR-nb*T}yFyw?FSU#-EE^=O0()M0UR?FWP#Meqq3*d1Ji7Y$=919r^4y<*GT+kS{Nbd@M;z6O#Pkg z+)LyQc)U5<5%x}7JY#!+9d`S2i=DdYdS}AKRu&D*UB-Kkt8C-gU+%=)|0+-AZP8C^ zEVo(z`I}@pctbYx|AMDJo@+mzB~XQv4c$*|8o!zi9i07z3Y~N66%XS(3vu37Xf7N= zmjZ+iyR|>SwF-qpmtCodJilm>`e{={IszQ>zwh6GmRr9EdHoJ?x^<kv8Q$IH6LN zKXGL#>T(f`db#jwq^k+5Ie)yn{&3A`BN65*S<|J4jxzsoL{;6)NcTA=d*e3EK7#(>yyXgaW( z9NI~**pl0xZ!#2`Ac7STI9`qem%xH!9)CYbIK^4S|K01NM!!Atd*cr{P8TS8dlW4U zX5Y2eA#U_)O-2d@_z5_W+iE>ncQ!dqF z+;`9YqE90x)kjWYA+7w2y!3x;YI_m~`UXqm>s(*a2k^8i7a*Mc#JHw+7tRPcLWIMuOPXOII z*miZk@6IYD8*1_!!+b&cousgF$rQ*0!yR+?mmkjE@CH^#I%wpqNx?GY_WNgad}|i> zJE5LK8jf=tMaf|d*W138e_Ltfb}j;4@dq9oPXqo{<`%cf>vt3+ajv}#Pt!C>FX{2x zycMRijArEzeWl_EXsf$4Tg}+`SZM8*=G|yqjm@%#vh-R~(!D%Ga7kaHEm4hXV)GRR@x+2QQK&X6BelO z4B{27T4ZM86SmrC;Uu{5Z^e$ILh&gC(0&499KqW6ig!pFuT2Q8zg!$x9R!b3SA)IX zEA+--=egC z8X1KR8Vlfv;yKey)lr*+V%8h_wMfNHnvaoSY?}_sW9GZQs1A9%jdgX>D$Lc?2r6mx z6+50MIxZLr6Z~UtT`UEGkuR>G8KOza;svo+?<|3X^mZnR=qpIyQ;uxy^s}sFbWULA z%BMTkWoG9S3{Rm;(ua9SITny>bc}1ZR+9rb*wYg2`Ry)1IF`bwwF!CBxs@Pb5TpcZ zU2wC9QB36Y@E3;0k}YXSFaOGny;<2ppfChiBo&QZKv7=KfXN(nEk9SN9{flqUt_f2 z`%=1M1z14GCH0%%wvb(B_3B~Z1u+7vC;i`eUpGGfA-zZmiR)CC{8-2Z3s_m}g@IXd zPtf!v?>^ItnO+jvLNB+DPPXHf|JtVa@$or9xG=S50CzpR`?Xg^PHa=cO;W%k>x zGud7TO3vz1NnRi0Lbcy}_}_HZK8Mx1AK)1J4{iCMzid5Rh(3m&evUXFJedl3*?L)1 z0SC6ccWpiW9jMe|9SSyaL=Vyq3PfAL7U8i6o+~8hkA8_zk|Kjn-j3|}3P_QnTBP=| z)W2^b?HJFOh!kA68H1>X=Sa@Pq-Lv-Oi^liSWpz4SdS9H#=PaDKi(n?)SactbOVMd5&q%)~vn z&_$KJ>^n9X@NbYlna*Z9|4l&##Sn9Ns{6);ZNe4kQYR4*Ju?ax-NB!#n>``@cpQ>I zG}l=j$pxLlc-w10STO!d4EHJF2mN;EfzkP%kGBilgtVU5y@RQ|+LLEf(igl3YQ@u- zQmuxyoO_;vQtQzM*U5q9OpHbThyQwj{8L_&S65e#ANP)gVMCQM)b9=D$)(o$HUMr- zx=kNPonDBCv}B$^YlUk?BjM*k-ErwD)hC7%Q0(lWE` z6#wgdvj~yXmv&?$4mv0b!K;ykk=g{kEb4{dNPYM-&=P9->?SuuwdD9qJ3oEJ&wz&u zw8A1Vp;8CB6iP!0PuD_Tf9?%M?tjoQD<9gQs62sJo{ef{*L$_pN5|pQ$4Z_UJ6PF6 z3aFBY$350?0REhE$B+7Z2|ZTgXHN(VXWA1-jh``UX*Oz%A^W5p8|E)?W+MJ-aOt&d zWa4@|AHVkjN5Ck*tD3IRM7Cvh>+LP<%`5iPrTWpum4rcm^sT1~Vgg8-GskH2$4MU2 z&j_JBRU`56V$CsLR|-z?FZu-n6k2Mc)VgPdyNl$zhRVeuvBoFm2Vd1N+UG6K;{vYT zZp~P7igId$BoPP*U*?#0PuPvQGhS~U7mYY}vtump!&ZJYiKbXPN@m3IzA@^+)7~N< zj(q3bh89k6z}`9yRC4eo23T-ysFP=0=ro>LL{H)A0hVETnjrz2f9Tz>TJ4TW*LHl!UOs}WPM z(q!NFV}{krNg9l5ucsQGvxzIeP3xKz^q{=w@1uS~@E;NoK>&-Wi> zfZ+OH?c->f$el0w*`A8;wU+-~PE1f#gZ)~s7Y5YsXdyNq%Q0TYuSicB_OKuz%-={F zx)k7UX?b~ULNR^*7&25pHncKDwiISW+wv*)HzriA=J-$OyW^e%EvCs>=Ht%2Ly%vo~ z^$2tLwc6hPXw-APK6l&tk8IHWU=um9mmJu?9aAB@koySmi0c@Gazni(&H_BF_nm z`ulM(paVUJU-BgU=VZ6a8Q6S9dsvO#X=$A&pC&ATSW;^(TQ6N(dP{c@uMM}SYjcG; z$8DzdgGmk-sW~nRj3OQ-MDi-lJg+{&&CATCU;7KS{&$t%?--yUz)atN+av@~AR-~j zq^9vQ%UbBSiPAYZ%n8z}1Ol-3!9Cw=d(Ka~QGS!Y{vm~k1e8+kR;#j-sl#K^TB+t% zYssbKRe~pX1ivAQJ?4h6jJzlQxj zqP%(WedaJgp-tmr{KRqp6_x{aZiI3~$^U(@7%bb}*BLG6GO0M>{ISK3UP{7?$!vK{ zyM$s_pjSFQ#=dh}d)szD8G@NOiO?*8-B1D@Zz2|!dvG(jdxJ3NXflJ5(|DIZpY>v! zRrXOcc1$EO@k0>bs){hjBkN|Zg@=rI{`sgSFCoxMZ;;t6GAqJWUi&V0U)@}0bZa;J zWbT$eP?=$PdQL!DiaYx7U=}j~f;zhSE z-D)O1Z^Qz+NXUT&BVncZq@xB5t~n{B0}cB)#wy^{nTTApeD{7WYrlr`nk@fW6dfhq zSn&m|alkD`$V+fCL^_j!dd;2f803zv36);PKYO2)U?xsr~k0a zUCEs|2uX)e$oyz>>xM|d!oo8kjxXdrqb4 zqZRD_lws5C66@r}7Yd&I+(?^waQM~PVn~DA0_=~xVjRy1h(J5 zVTd{GUT`(CZvkQz6J(k03wDqNo#Xd>bf>EvHB=#suw6G=%PjeX2zrTZX0_Tt{#rn* z#=n9lZC6A<#(A7|6h5luqmbH>I43OuiXu3O1S^l-Xh>Z`2SGc+Lua|`sOBsXE> z2;aq71YNRs=K4%kDAGGCqUV>R zw4vR&n-foxws~lS9t-M|Gp$FYFtN;?eCzF$#Dqsje9J-Xx|RLbIhE0QF4no4_g`n6 zC@~%@0SsO|xihKvd<8Ck42m_tdtE4{xL*AmNUnx$41SBqxmq+KRBDW10JdwjmdoGA zmp8=vy1|^OC`oM}yHlRQf78(%m8!0Sl_{5~D(Bx4dG@*RUULlCOY7Nf(L<@}d;)IW zbZFI!!>r!DY`qbB_@8bEyb5nQbum&oXjFjjn_NjK0PQHFKq^u)w>3T!8iRZqs88Vg z2*0(KB~~1Y+j-VsN7!4>s8OG0t3pO_m9u147xn_)N(20!oL!=YTh}`~R+3}&zUlv| zK~6xvSSXlRB3{dTY0qO$-D~x`*+L&y=y^W*evL8w3F?{v;_2gGqm>%lp??_~#YU;%sx?)XREXdS1d=ZfIRCVm zAjn8ApLf6|j|jx@u_&FEk50&0D5p9YZl@STj28Ma7xQ_IEAENg^jgD)zk8X|{=k0C z|D@IbBq{H8x38~{7ZNJ(-dB(2d>$265+&BLq2mg6Ie&b7gh;Wi>&{VdL0n2MO9mc( zkA0*q;^&8$GsTs?S6B59-6~3pZngxNjcpL(Y~Qwf-&P#)!uLRM#@e!65kORf&`8h;dag8AnR$zKc@VEW?J~4T`;TwPLxS;}x$LY6aQ-qLZ7!$bI8X z&3$Zm>JwY@LyLmJ-j0&h$uKpJ0r}p#Rq|b)06TPj)H{C*zfU$BPIU6XoLbFDRv3s& z&~o?9^)sh3XTbqU@*AaK44odNt^MSB($?b7Nj`vdU;r#}s#v%+Qi+_jb*2G@=_sq2 zN5a1p^9|n-hd#8_sPA7%eiWBYqMi?a2&jV|N(Fg!{P;QI+!jd_Y(JGIvjeQ9%$2?@ zx>f6(Q7`p5x4TYOFF_V-Fj&6BqS5TZv>?RGL1e_5<**Q*SrJp>lZmup>f)?dsAf98 zKulFD0C&>Spm!3rDUs!9Wms4rIFvxg9w784s6})B%;}eL13aF79M$llsjWIdgY9EV z%2-e5pF&FOq2ukNde3My-}H}VK-c+Jc4o^HaqkEOvP*Z`WM0(<@Tizru&sXHSC3C} z;)uUO*hwNcHvRZ(rL?yT7bqh2JyYrU0e$pK9+-hZqnYkx-b*D3k?h#$SRqn8`XFcC zPN8pgesjV;y~wFPn`IO=BH8OBwiyq`Ep^NVI+k6W^#HxKrL?1 z-QZxJ>wwdtyESKmN1K+A*H`1N2s7FKBbSO{yngh@u#^d{J%)7srv{~ zYg4LvR<6h**P1!n&8BlEr5>@%2R89SAo~YowPcz78d>pe@M%@h2XZHxHtJ5;=BuQ& zD2O>Rovh4FWLtRX>&-Aw#zlm4MH8o`dls)FCXZHPbXQXK8uVmwfSk%p{6;B!%sK?# z{B_i(iWJU?c!ztyNeljuU1==m6;~O;qf170G09Ber$*51o-;kg$Ou^%Md%}?AhT>9 zTC++(*=in6ypwkK27w5*Ak@KRPLhbxSci=p+i&f_njBfdj&0p-Sj$XxzEJfxY1u+G z;8+7!oQmJ8cW{rj(ZP2Go$Ofv4V<`$jqS(dpu$Wi2PA|?J9rT7__;f7t5d{K-|KOv zMOz@eJV92@6dLJdN3`oj<-zOiuO%t^cRs$c?>oey6ASws-ycN%J|J_)9)53Q->*Yl z#u9n++Al{8p9b>$UV3~Cui#oYZki46u-fY)11lvZv+nv(eXqM;Ca<1nc9g0$B!9+C zVutRFJ-n?*Xx(0S;j%bYHC*;|;{?=|$j-s*W-3V1pQC~zRCzL!8`Z=nvKIKIw%KYHV3%0yt%uPQQ> z8*_*Q@np3yM4_4L6Z=1Riw=E%%RnGqF3+bUrjxy$r>GIV8$p0*C@&WQo(7FBQX^9H zn%b*HG8Zq6pPPGL|Axvy!=G{uLs5wRK|hr}T=hu!@18u4!FL>usMuxhM|B@Yk#t-h zF;+^q*(F0KZ>CWE(qV68P5lbAWzzeK@C$x%l0QK^wS)pk%9v$%vk~;zAV`rX=#h$_ z<&AABVbg)PT5P6Cm_U7|FT*dL9Klo5fh91EnJ03!G`mxaeG)~*R zl@*)RT8MD|aB1kmH)=c-$qH)Nn=Pz0iA1UxO(ZT}v z?jZNa+n=7-?T`1g_p^_i^9Mh^e5`(H@jV8MRJ573r>`7u3M&1+6924oJvc+gQ{d|Q zT5DtD;->OMKMHZX4t=^0NWCsToBnl&aeJ)Q(_u|u(xc79lQr>Q@-U2SK~e0=7X~1(>rjfd!}IDN z>g#V*?E!*YWJpVPZjb6=S?So5f zq?(x6`?CW>ldXJwQ4!@hA60eHEA}9>oI-O{@zIn4BQNcIO`alzyN@zch5S))IBEUh zdqb!=^`4CgGnoZ2BT}D!bcwX&F`<5&bUtcafIlr}tgLndhH?P3L`I|Oy5KF7)kMFx z{E*;|&CUhrDg+{ZGe+)aa+hDB(E-|5t4jAZ30UccBsDwLq$l{vT660dT4V7Kc*igU zYWI~Oh_og_8fx~$bc+R>?HC0Q4gYSU0KVD<~a zHqyzP>8tnvj7O);sF2x^01j5?ks0N&_4|fRkQ$j%D?jvfGU3220ar3bBYP$}Cpzbw zz;;tewSz*#YS%ZnG<3KnCUw*GZvUQ`NcoxxaYZ z87sH-D3_U zH%BDVwIg^I`kdRo1fK!mnhQCwCk!0 ztYme2yIYEqolz0y98wH25Eg&oi4b^-wYn7;fk*(=b@cjG8IHh90c3%|LnQd7OY+E7BW7{_uT)WuDq!sEAi6*|G+OF}47)y2s zhshstS9-U=@BG7Ce<`gJ436Wqe8`)w(UL+BLdID#tSJGu(nU3N|0-UtiNkprJG_t0 zN*#{4adRz6lG4Y{&EkQt3EFZL@4WnDYn64EVWW~F%P)-$>?vQoV~}OKzdy;3{JS&r zrx01|pPQvaTf-(|P#aFHtuB}NB(}LEfY*G$)&a>4;K(o(gNAJ$T`xwy_q9NZd?JyD zy2dUuMbKw%Mp~}--Py4bY@kzJ5y&I;a@gCs!_AuZ!bGU*L^Y-qjex*HWYE#A7LwoO zfmM`>RbIYkE_EJF;$;_9ln=`c%@-nzmYlF$fu?+R@}J1Fkq;jCeSh_GJP=j$q(HTK zG5t{@KVfrZur@f@uJG4$|BHCl8#GrSG`eRPGNO^{z-V+Sbfal_Lt6!3R1oTkj9xhH ztXq9C=K=*^FC44qQs?FZR}4L-OvjGM3fA3fO)-8nyZ_r#fM4_sLshaly*v*1x<1Zt+=0wo zCk(BP_wv_eC~K1%BLo|f9St|q!j%|lvTH||Y8>;8W`4L+m;jmBq_Srd~8OX_bT%TZQx<5>@)(G>cb6Y(dX?3~^J7m)Mh~(_*n%rehmOt{_05W5Co3Wzd-n=d)a25?6+xSmK zZosjD&hR1=TabMx{HbI$nic1Blo3?jN|l;>ky)Q|H-ZJ;rV(w?S|PgA&1MX9OC7bjCcWdIyOn;oPjh<3OEiXu>C5{gx9 zLAP?)od3!3gw2@hL~+>o6qu0aKoUO^v)+E})m(!~5CH?^1{>K)%2}jCw|6`H(%?Z3 zI|sqDI=eEx6BGbfi_zwl3)2(<3GOqV!rLSPU_%yqNx%{sFuqW2U(JdmYn~7Qm5xrG zrX0tM0VlXF=sCpD$mNkVn3TFRC81g3|6Ixq0CCIh;Bo1+y^!b^e(aXeqS)$U)7-)5 z*Vv+y&wdBlBmFwB=k9s=Gj8#^@+OeC<>agk#e_`e>gGU!z9*gRK1wG48t-7Jt!WeJ&~R5$1}>B`aOZl}%DQ zDR-}N{+^N+s&b>yg@g|^JWeaDRMyVmAtKi z`Rx=+D0vB-X4_PbI2nqZ|0D>Mzart}-f1XM7Cw*qMfZ8I)>~qtM8M`Ms>N?UVre|3 zL#G_935DRzcJQqmj2gXgYgQ#>`lM^7P#_TA@LT9=2Q+qyX~Y?LhaVh^KL#am#~ z3SK%{)*MUx1J<=U8G#H4zBHJ)U|IqV&#RADFLHEvx%)1dQ!DSb6BXj&=~SP)z(_Mb zwLh0mI{wO2j8ec_gU#`SQpbeh(li#>vDUE^YR@_@n~=0ME&mCmny=s>4`A$K&vxKQ zc}$c4bf%)S7Gh?A0Ew5pP(pp^a&ns$H<5@~s^vHg2xDE(qvS6G>`waGUPq$Oq^uVk z%l!?S-!WpTNjzI4o&GF;^Gp8Qq$W9pZB?xXa>$?h@vi-k1Y1jdY*Ho#E*ngO23Por zFV!8|hAtKPZZfI{IElSyZ1qsCU4d1|JV)}B0*_#0FKEP%X5qY)g3u)5FH)QL<257T zA(fYeC}3ai<>l--@<7|`etq-fAmIA2jMOf3(3VL^2K#`B>eFd?bWm9Nw@#VL&w1Y3 zXib$S138*>l$!qW6RdzIYr;2fnzv*bY()~Sg#nYRDof#$m+p&~`-g{seM|p+zl%Jg5iAoX+8D!@9r!2|^B+LRbdjc7MO2z&47w*Dq$_GmO z*RXTD@3~8!?O@`6yr!E$(L-)yIbx^l287n0@?LC|+~p0p;a$PD2!_<8n=k*VaGa

    v5OuU?W?}*V$?=bQ<^kY52aWr#ft~FLXDm~KxL&jTxh{$fK7)qi9dK87m=W{w5?bcS`41q$j>+39 zBzw0ikLeB+yd-Qy0Ig24uQ;5W!<`Ku#ug$Yq}lh=ADNtM#Y!lvKG(|Ht(0V89KD0q zrJ8m3Z`u1i!N2|~SbiFLoer-^V1h>E*ihGj44Mg3uVw2$70=D%wX6RWdGe=FZUuNK zh$d~8^F~52`y|?NI}FzC5x>Y3EQBx-c`^gpQ=fx%Z@*}NAcqRK;>c7csF2H=-c1bE z2B|g~blkx$@%fHn#+R>DX9TmDkhg_R8)6Ti^{Ch7s#RGpl~EM z-jn9VjxB_~ZqAGooIFB=M(S({t(R_Ftu+70(w|S+L#N0a z7l91u!d}C3#4DeK_eQ= zGR!}!+|qv3q&LD&&`PVOXhMeJinUFS!bC}sQKk_s@HEmmCWzmNqeO&Nl@}2%>5N&h z?mH_?M#*uVT4=~6%e=5LDV$_sJ&bv;{Q5g1Plq70>uc9-i^kYt44q`ZPO0OJFkX$x%G83D%i_sQ`47n`TcKILyLwoT~rqu7$iC9sE)Ju!y>FG=H3+-K; zcizNOSo$gEjQ*4ivAXEGEK6ET9W{)fjHNWV3c2MasUxRXUm}Yo%`~k~Bp&Omi|JA+ zlyz-jeiaSZrwqy|j28|pG@mCF(b+a3Ylx#zw|^3!j-iaGlW4GN#m#m2@ABlC9BNwU^dIzw;<{Z?39k+! z2TE%`Uz2l@^N>9>emv+Yq8soVI%?Kj#ztvM0To#Azu0JGhH^;~5Ocb$wZK{!x~8HM zoRZ$Gx%p=R2;Wa_1o)wC7x%4Id{!Yrbsb>v=JR4J!~^AR6+$2rGQ?Gr+H%W%P9j<$6+QbIMw2z0& z@WJ?ulMzBloF$fprF;?Rvk1GjK8gIrUN7zSxfzg81yWRqm&CiIC91?Kd9am!fxh^ zTcfaOF`r=lmHqZFCPL-@37=s;JnMPwDhw^V&G4=zVqbf}!BZH2nNwmB&K* zL~vSa7vHhb+R0rojXvOB5OXDj@=w9-bW0e-M+qQFNXjf>J-9-i#4k&D;xF?k@ncTQ z14j-)6kp!`5OIsokVM_~jwsXvVv8+Qv#BT1GRsM+qprpW?WgP<_GCSLi^-?tGeP&R zOwn}I7Dpj$8cJ*WqJ(|>7UXawj-#>MNx_n5q^`YyNx+`Had#XpO`$)dW@n3NG@LW- zn}9%^>co@T`>^J+z}OQ+W;fs()=(1NejQ0Z^FA__FKfE=IEzb}B10#MD*it@Km z^@)_U7D9C)7s8|IPe=CDh@hKFclS9tX}Gylo^+9(Xz+&*2?{KX?o63=hAv zOaJ{3r+$z8_Tqa=W12Q*F7Nu}^>qYpu_48Z6gzyyekPi6DYcs^v_ufdJSMfa81k10 z6e1bfV6&$B$Z)tC5rQ{zv-$4axDsF1f-(s-Z%+1>02}C&!4PCk;|yRJ6R%q6B(t~d zw$VjuzA^N3cB=xWSmFoiN+{D42#8EwYtRbOifF;VFcSo%wU|xT_+p}ZL(zE1xd3C= zRkDcNpe#yp^VbaB7}C4!Zjo z{R#BqJEi=~qLg>i%|9t@s92&31!}Z>fCovq;_4kzhy@H|P)^;;Rgh{|1L%p1F7=6A z;3BisfCc4v8Q!XxB>=`gs9d1csyb71bkO<*{fGVi!~f)CMp%_yjV#TgRHE3>6?LmZ zS!SYqe@E0!X7Nt<@->VB9Z3WVP#_)!Sty6mD8cc9U!ePn#h?9isvqC%!~!MJ^6x=G z_y8zq7=nD}-|>is;?O2_-{2UGq!CrmoG~JTE%SduNqMbfMfK*y$mh$3enSLN0fAL< zD3uN1C9RAV7xcf)bOSZE+n(rJOm47JS`AxTK@xpbiiOd=f-yf*FwwbtnBl`*^)zGZ zZgAZvxs}qUasrp(EzU8p@)_&9wVA4X*%4K|M%DdyW&C&7+g&a^hXgkv+nR%oLM z#NxTh9)jcAuQpsW{9)(4%UppW5-*43u#V&3yhGNay;Qy2kJp1d(SY~MOSX`@*4Bq! z+-_CL^Y#N0AA{%_Vy6Ah{$93MoLzRO4QLX8ph20 z{~g`Z1hdMKFec3u_mHV@6Vtm$xO0wwWUD(m>uuisG$J`E`jOgiVYGJ3{id)Zcn_sH zW30Dr1x4JPWa%1^eK`9v9S_%SOC}GC&Pa7m9ob(Fc2l2Q=2p`{QdOdeUj9J>@RV2k zf{=MdW}!3lY&o&RH*xWe6u9gs)aB|4ht9tC;bTKc&AH#CIawhMj9`e(!2s#}Q_f5MRTE6D#Q7_k*~%$o5Z)0U;d+(hnN2Z0L{|ViPKuK@~_|i9~_?8yCkAH6jOt z*8NF#e9?hEvGCqavWeGxPbeeC2`6O6DHIAyt_M+(8iDtplaksn9$77#gda4j%XicL z1}5lF$iU_{x~O!n+A}m0)g-4P_&<1K_C+l2Gc~; zH(5z&3_!_Q2FMvn$V7Lne38_noVQbVmHk2(Q|Sq(><2^ct86f zo(W=t4rJuCrwr`2232R~JxI9u)lE!;yzt20+MK%IitRq#^Yk6&Z&%oKa*5*_WHOCV zhQ%McTuMrJR{vc4o7}UaEG$Lknprr_1w1=7`|WjhG|TJ&oktSmrx^O#0a@ebl?*G@ z`)M%Dofs;J)^+2fDyi{PmB{k-VF1dlgdr z%ov#T+|M`gbd}r2>I%h|EU3tFkr~peXdG>8vCko79`yYasYX?YB16Zh{(D^0(fQwk zgPs>ag94);g`|(zSZkp6e@k>F6oI7$PlBN6tncQ}+XIys&Ky#<#ywO0ct@=OJY_Q+ z_#iqXEvPT{SajJ-`Eald$g%op5S28(Ga-C_zb`ECSZR-z5xa z1LSQQ+N33abjve;kJ z#zWAAo}fSgIG-VV@HdKm>rXy}M8pelOZAGA2JpuUGL(YhccFB2Wg52F!3A^O?D&1qf{E8d!#$7 zvcIlXklu_@b!f#S5GB8SL4^hf1|rEDq1Mr3^^m)-a%7f6Srdj-Hej2m9tG~V zk+DjC!T$OWbs1abC5X2Buh&$^_0h|D_X(wpj0`~#AJl2KsoF&j7c=}_h@E;Wiudp0 zlaYXlWWhh@NLdpW_1eoZ=ULrHSrB0Z`q!yBs}Cd=O9KcY57C$-|5ZEsRfb0J9dsX8 zb|0tI`tEISZ{IB$Znd0Mw{AWsi9RJ&E*vMwka+D7ZfU3P`E0#xrgh(zOt$}5H3vL~ z2b6;iUk42BmRbzo_YA+Yq)oQ(?CfY@QkQ&!bS{puap!z7uiNUj{A$xNR2cTO84&G5+0%V_EHGs|7!UVeJum%q{a zg1eD%!S6kw^B!ZtR_P6&E*)ouv0w=&CT~bLhiadH*kXxZ7;-=M#l#b_#<0H^Ez1zTk z-*xhRoX>qI$Fh5KPT#h6o=0%cxn0rAK=Dn0-aXOVYK>>llpO?MN@NXiG?AJSN|`W1 zOlDbm4&2lGd{}3oeO!6VOk6 zW-p7p9ziuS#DUJu-zZ!|-F}CSIjl9hc+o229E2qd-KSq;(?E@8TkAY7bzVG0G*#}! z06y}heZRny(YB!_I-nfgeB|5PG&?GBbk>QPQ6m=4C7_b|3Ty^)1`HJUAr-h6q`CjD zrZ{0QJ!N&IY{OX|l})+}bzs`GF+B2>X4f@u3>`_zb0_DtPI^`}#_W`QE)IX4LP|P4 z4gqDa>1d)+r4}+=;oU8iIoEKGCuO&aCgqUT;s@KCFw4XZvb%SDozB{9o+1YCNY)C! zA!Iji(O=V%66B@M`6{8X?XLn#}&ro#d9B!|R-x z-$PnrxPPOt0_g`=Z}94AK9}EIQJU_rH}s4fU!T6Z#2&M3ZE0{hTzjHXS_~t&^)d*C z{%?KCLbgynx`$K@QtDw*;AnzAAr;Tpz5KP2$`Z8$>iLh12T`1DgE5VE7ti>)MiOfk zmGtp{Om2t20t}A%kI*n~Td0ZOPhDc%wzLQO@aZ}F2Th3(@)hDn4##v9l;8g6qH};F z1aYo2sn=%0G1mbH{W6s%)XR7AZYq_8%_X9ME8ft}1-~|kc9N*cvm{~eq;6QRK*Rn1 zfTSUjxF|T!BfV2Jf+wH4FZ^3c{r|SCIRvk@S$|pP^uN3``fVgcvJ4ltg3ftumG@D_ zr5od3vJl%eD~ssPo%s>AG9-kfiOAK+W^@_Ug-2+W&+gwtx{>a_wn6f+bARS@@xXVW zVLtedo&o@5xJ^E6thKJ4l!#Za-7SoaMnhiXpy6mJ{`hcA#}oJv5eK{lGE|%2Us^v2 z1pa0jDNe1xd}>_Z5=61Gz8Hd(t$AK!ClI>ft&H^P)llP(qytlB;<`69;4$uf=ETrT zOUZz$r^EY5>*ML;_A1VAmoK-&8_8x^r)N#&|5&yik zw=qa;kNcXv!BPb0egtjq<5S+p(|?(e|MB=;51Wkk!ne~B5Tv5V_iUBh$!M4x>nN-^ z6j$l!@855(vIpyb{)nyB1RRF@BmVx+KjXg}$j8sWvbL5)$Ww@Blc-3=83Q)+F4f*V z>_WCkWi!NO{T|O5D2q(u-4&GnR6j&wYH_52-?)k4o|)6iD9U?3qMfr=a<2!RPMX^p z0OwmUuX^9AG!vzWIm>^RHGEo5Gtv$5YtOQJW{k5P!&^>u-N9RY{;OCMlpsMpLC(yk zZ^1yPA~l(^t(8_(43Ah10XzRCk02efDSsrM@eKDaUMlf^ad$r)QCIdD=cLNuSCAC4 z4z8Obcgu!X^e0ZiN z(v`010_g1l=jNrz;}y9m%lC6;ZM?rXA42yqV8Y~V=(t;Wm1f&R0Nei3r>mumUf3KN zIHs_0@p0<{&WVEHUI<|+OG2EyrTfZ>M7dy5%+ixXt4Q+I5X|B(XY``M>m=FjJ72Y@~YbJ{p&8I z;%rP3{>_DQ{~aXtojv(u3=W?l{D<9|LmR4P&1&D%2oi@6WJ5C!!~fq~w^M=1e)@bk z+NW>bsQIuK@Nh^^k!l9>ug%4J;yd)_URz~}ic6X%ABj*Ax6_1Q)-Nxhpl9rO%x{i_ zRiR-cLFnTks`vD7R>I5Sc*KEobLzVKUtVwxZ#pjgk7rm#pQjH{ad2Y==6#|Dc}1^U zNS|^qemy*F-JM+ceY~LN1{?(3#X>?Tdfq&`?}z&gy^rbC%qZ?z95mewLf!dR3G0DT5`sgwL559ThgpPE=cWH6C3j1{aCd zz8g#r4-eepY0I#kH9m(0;r{oQ0qy|-AF+bxM*A|Pez&X5wiwCZSi1Z1vP#=O-d`YI z`lz4%#1SEnx0!{TTh-DD*je<{DF^W&&=IF0;sniDkx`MOlT`zL zNhuCd4G!-pN>L}DIqV!X819K1L14lQ;xjFCQsXcZ2doHj(eYy$9(CzL`XMj9p?Vki zoN&1sg+t@ddb9I1{nvQWKm!?mAC?Mmp6&L=Kk?BuD7OZ=vGX6x*U4Yz-1iIf!OrxY zoz^}AfO`4T$vr~s3Nk^7{FF9!)X$z5QoVXYdoIG~ri=yKG^0lSWv+x0&lEMEeLhRt zOf!aMzjA4kIIkvYx7&P7lxjFW5F%BL*p)u zySuv++}+*XgS)#2cXtQ`C&2FSmwoo>3+}qAsak8jYmPa_Gw^Jr)wC(Nuf9oye=8rD zLYJ>)$B!!d@B8n6Z$hV;kbnm-UoWqYv*n@M7=n}RDnd4i03kByU^I$RZ;}fEPoU8I zFgjP0n4Do7c)Ony;e-2HH&);TLALzPpLi;a0^f&k0Nla(BBV0s4QHjIIp z7JV7C)=pg29duN>zR`A6!{jlp;{QS>(2k80W#xF0vU}6>cP7tcl0e|`?el}v?;(vb zFLcuXMFZzR@a^Vfvh&*WAx-d4e z)C&4N!6uwoTleQnRTxCx-Fh-b`&DyA4sUO7g{ENa!&-GmZBo+$Xek`0%o1PNsr&5k zd0Qy#w?2ibhC?b6^qo_K2H0y41_DCF*Nc@byE`>DPT(>tKg(3=;cTv|f48~k%~$^u zEV%Wnv^eKzGrf69Bmbyofw$5*OHbp+x}ZZ}G9zcjf$8a5?s}c{7sW@j#@uRJ27{Dy z3m`*Y;EJ%E`mErH8EMbT;RdfQ9P)Dxd$AscYc!=Y|*ektG~5(a=d#58=@?@9`qgj)X^Ef5cZn(9Eq?8Y zCf=q;813`kW~mW0DubTpvnCo!rOK39Wp{1Hx^$GznMRyZ7%Rx3YTbw#qa2zA%AZxj zs`dy;Yf;!TQ}yA=&Na}y?=_Zw8)4XU0@!5CbRi6+5!gc~=?dW1?AX{Q5vVJQYpqW< z!Trw@mN@2X3OMk^@rq zT)M?EB#1#in;q8dbZ27gab)eLysSMlt!G?TzMjTwJTauZ0QXuh-TZ6Eu_)jKd_-cyzqXxD95IkuREa zt&xbrCt~Vh-~_eTTHo3R&vD3oG3eG)-;Uz^sl2Y|7(AAz9Gy{uvG(q+TP8@!B>4>w zOd0a(7h|kDWpR$4PODm);9f1)FNAdJ!2II!n1uE-PZ{xuHln56eCXaU0tEG--%e-j z$}GcA<|y@d>tDC1@!D-tWzib^wOu)V*HzLMNDe=MUhSa6?RT;|yikg0m1`P5!)@p49a5c(W+2ZGGT6HU0dR?05zp=hUg(4MSh*mx1Rz=Y0 zU{Db3+a}YzS)AqR%D!EAJ?nngH7q-$mjod<08v2+fWPlONcukQ{j8A?s#yIzk;c!$lmw!=k`r#I-aT;U3)df~$y*s% zM4hH>e&3?1@-2xy-(XdSvj6vu)hvCF0Fk!c4GgN{z^DqYblZYDmHVse2|EgLGKLx6 zk6w9I@1f}9R04sd2rUutmVCKgQhkSlCA>*eiL~z}btZdgg@~*A6rrGzI5KJJxYKDh zB!)pT>jNX6P&&*_Wv0&Cs=vbM5-S9fo7USH_PXgKBJA4p?s>bFAoja2m|d~k>y{vh zl~-2~@Kq(*#MtYSfG=?RHhfC)c9lPm92pjbxs{a>C`>sitA)+ho;UBBlYWRD6~d)7 z^g{NUcpB-dEA!G~n>I-a@8n*5!81+|=)t6^E?Xj1*We%kBu1Wc&i3yEP_KhdJeP3n$%KJM`xolnSIos`*>>f>X+dsP0DwQDl z$q+mu<1D9&8_iD9QU|k;W`q@qiGyl=ywo5JYp$eGH4ofLvC>$(3-1cg)Y2s4Sw}B| z8vS>x$k2bqhW_!<t>?! zXxvi;JEP#nNLn7$i+?2`|4v8Wq!FW|y2_uyO?6Z4M>ni&P(C*0lr)vgj{Ov^sQpvN zRrfLk3B85+vE^h6Uzt_=gNg+UU6WdR#PUcMohH~MHo=-QPT9#JmT6 z^;4i{Q3Yd2O$<15XoE|z>ApvvKi=9%X-Tlk{+%CHb95pip5gU8rv7=un>rxrohG0K z8C-qHO}uz*gK{}$6Yjpo%9@ir_a@;`TtJJopQy8EbmdE z4(MA$6pI(vTJcYD&G5w;CR@(8UJt@|Dkw<8H$+O+!0gNHHys+3?yJU6J|sV!Sl zUqaqwEwgc37?6FGZ7S;e#b#n3ShuQ=)!k=2fI_3+^C~Wk7;KJ6z^-RFPhhp z*~mjm^4C$St%|%VuphTuZ9BDU?g8P>cFkI#ZR=H2CFvvScn>-3sA01vN8&`=-0&et z6;KKmY2m?vad%(AiU5$|e%m>k4BAm<_48 zw&JoMYGdZDhQdd#I;`< zA|8jG_*y4tfla!8RKfhH(W(}r{Oq=9^O7%|=7DVTyN8ED{odb8g9J8$23gx(@FTaT zGK8+yj-qXT)D($zvqx6uFqvZ%T;jJdqf(oHiN+jGZKZ5-8;-M{Uz|h(z>1kw{?!Iz zCsu?kHW(!&K>%sj^~y5X>s@%t`0A~ri%4BlZ|?`CxP`{?gvN{6CBJZ@@$p*r#M~Vz zoS1)969dpZ9sOj8@ZQ-PNYUhgG*=b;W7*9bxxnpy;}yh>4uyl@L-U_U!GHZzd-W@eekJL_{X$&d%F_CjlgYn~- zOa|K zq}4<(Jbt~zlbmOq^i&bk2r>I-2k-KQJwQFAO=8vBVbbp*F)ECy z&11t*v~|m?&3|KWAHadQ`N{pS=}jPZSWHn!Oo+NCV{Gd3`!WhJHq&ZPm^$kKb=!Z( z|8Hy_m?!x5x%Y+S?NUbnv5~~%?~cbalfWBwp2sfD=E84erNx%3y_(j|9+$E>e^*WQ zQ6iltg&E!zFNG`v24Nb-Ml1b5BEmR1<296?Pxo*`D6opV37jw2WPzQ}gq6WmTKX+? zn0zI~&sh0B;psN50xs%JSt`jO;9eJ}=+8nRy{FDGjxMvMYrX=b(kwa|M?Ac|XnRp6 z^d>NcfEY6}ad+eV1l}{cGmfLUd0=k_X2L2U$JuLxDtOLScaa|C_^ru|rvO#v2Np`@ zR!jQb-z`(=sGz0Pn!+k_I{t5AYNc>mwj^$(k;G?tuOqpg^eGy{W$KP=g#AU33b@TJ zB)k#^Xx#mrru{wtu_$qz!=ck>hNb;b2IOYgKp@azXN~_nyxEM906v~h28njE z?B4;lAP`33*JCUObW40x%6wV6VmxXjY!QdrvKmFI(oJd`Sxd=U90(AoLMF7`HdFjC zXawTdmD%sHw*7x@Ujc~}SNt{b>qM4x_&7QH)zQD?uN`iD7gZq`^?MGx?K6AIY&-2T zOD{S)ue~yy?fm_FD=Wbp#o!Dk{T?W=%>3Kc_u(8Ak%nuGmDXYwn=nKR97=eouYXkV zm3Pw6K)^M=_e(yYE{e-mqawC26{`Zarsat+PuWPSt7#9|9pCuNi9kd&EQz<S%aotTb_rZSX7olzN^UN__uksJHHR3z)Tbh#GoiAT;}x63KcwJmjn9 z@35>Sf0;EHwpT9B%j2r*L)^U0O(yrHLu!R?L_&xn_eQIgLzh_OzvYwG97VQtQsHtQ zlhpx7SVbT=>&8Nwn=^GyZ9JN4z63&QmLW9JL-@V~@;>j##?d$gJgQ(~IL({lO1H%w z@(4232f&DALkx?WTum+lSwc0{#RHA907iG**x9uWhn@(5ncc=mH7dkyBj+?2*VEl? z3If5w!8QKyzUj0x&bbfz9A_biDDJ^VIIprqq&Z?qY^YDK;uKNyKOtn%v?2(mOb5vU zSsuM%dA|cGe69&0Q|hX4d5=)a$rTIv8PSV1zNgh%YOkWpMu- zvEkdr3)QX}`AoKj38755zWGXGg~oBBa~T>K#d50d=;MhqB3H6OYienhn$0P_V3-04 z(e`n;d%uZVVV;XQTfCkUMJ4$E8wa2&agC#O*UQAsSXg13LPA#MMWe$5^dQgPzmF3t zqIV#N?-Gvn^E$-L8TJYj%$E>Ruyq7L1Y^quSu5;`d=XK3$@5zzxb7ih>J2zya%c-S z{|#+)iHYlRxB&axXAi^gQ$cXvUW5(ZhJ!F+uQtD$NxJk_0}6snieIs*Wg(i^V0v;#C+?&Qbn#sd;l451aZXuH-?J z-gPCA+9jW;zLtix>}7BI?y9bDMzz~UT?UPbwkIyEF=j^P<>VoCssfev+Oi9W=B13; z$TG8_`5>|bsq+M?t!(5w^kL!Qs(RYq&D8j*4Jk8a^=}cwnJ+Ebn0}b`RQ8dI zBclz7EqX=#)UiS40^j;$KbBI9{qN1NYp_{*-J6LegJ{d6dA%?EZ{KN%JXW($bV0 z;?Z%Cw{qmy)%H3JDsr=B1Mcrh-%Vq3{h!_FA=k80YcyLb$B$VvG+o%j#!+W42jqeY z4h&@i$D0E(sBw5wgV4mDW}A8o`#1TZD|rOVH|PAD8fH?n5?R->8Uu30#?l!sGAEW2 zZyT++b$e6+7a$CpQtAvFoJJZpDQ(`UoN>*8gQ&99Pa03CVxg;xtxW;3e+8DJR?B{C zvkdr9ZV#=4oX3MoBu(N?*BeGjm}GAiI=3>=$%z|63A5ooDrWAa&u1B&|EmFk)gVr0 z)x>QF|NngSTc$4-zp;@J5^{l!MX#qE7mJm(kSrPMqyfBrl&D|7Km&dt20);~LBql? zLD{b8>ybgh1t@_umbq-vm$|wE>_RHnt2s$LPj5N5{tl=wd!W}xSP;qsx&{aJdHwDx z=<3>@hU172)OX%*>p-ox%f!>fgvP#K^Ds&v0;(tuS^L7J9_Px3_*o@0vFYcc!b-w3K`u9=qa==X+LdPbJ>Xot?keP9g z_VwJ&01+E+;rKyq=br453f5L`z;G)sJ$zD)EUoHecWGU+%3n#XY(f(G-DXRE)LhUr zxXD79U5I;cW-4p?m>nFPGz0S?Io8phUQRMH@_g>3*y}bH4c7)&hV?>O9#oO1e{n_E zuq8d%ys=qZXmhf;@-R_>>{v1_`el`}huq*y!Zo*C*YQnN9c8ko81rYXfCiY}CKw8Z zKZHm4Q^L@~P92P6DPw14K~(LLtukUey1m=*{i}h7H4w`iWTxM_4Y-J`r}qPq;LR>8 zn>e>F8-`4r#mpkd__@sG`Pr1Dg-PraUj&-sR-Z~P8K^Vw;zm$|OWmlA%-W??R$Mn) zFiiDgk#<~HbZ5BR8E)r9Q6o5?PEX+jp1>`kW~tFCMy<*cb?jAgk0lZwmy)Z7rBXrP zeZED6KA+hv0MWh*atxTkT-(jSMx zl@$9F%>x{P_IPFzE3JsbSz%$-U~vyhPoXF%1EP7zwX<3~UZg zQ}9l_d_Pi=Ap-bIstH&C;$xV1#=rj83t-7?wl7dgeUSn*OQra;|MF+s;99%@UGq(3 z3VsU9*k&WfS>A2H>Dt`ipQaTz;o3TMZe%rrNkUW_%JQVX1gBAJ&Bqce%o(B)1Kb?9 zLZ8n4>jj0csCcg&KK@?*{M^x2v>qsb`@Ww1i_+$%GTuU+14~pAjZtvUu}vxfGvKAvafXwnQLFr{nE4i z|L$*U6(a%(;2jdaR9%(Yu55d+F?-+8-E@O13PWiW=h`qK>Ma}G16o8|#hZZLLh$em z@ByMn!qBnskd8rWcT?U%U98`*-=6e&^gdpLJ`du$?*_C%XO{va~>4{z1= zJPk|q)xB@y^eimtBeI|Kt0FJcslEx(X{nsDUqNueR(5>V)u>zVe&=6tRp5X4E#*4Z zP!yXJ_H^38o>b*4*6@A`_i>L1(x_XBied^6$I^|;pZ_!bA7Lc_~z--c+xjmN_d4rIT){>d3( zIYiPl$O-aMJA%dH$FxhiiZ0XMT>n!USVgCti1XqW1m4pSvCg^-xUtvt7#c4gg`}}N z?i^R_lnk+lCC3#vHqi0_g^DT3d6Qpa$~}!^ZRVHP3&kyi?`wfCP0&BSZBk{qyQNbJ zt|XQah<_0$+v<+*6TsyyXEjVA63C%w_BBRE!&Pw#3+AKtrOIlVvccm@3XTNHR_G^l zQW!*;DaH@orBYM!Afq?i5{SgdOF*4fPK+&?jLQSz)1gzcnGYImeGF%2S>t9JQ!iuy z^FIn2(>VD66ZRQ%L3lqYdb}pTmtanZN(UXR88EUdK7R1gC*N+wxL#%Sk=i3xFZ2o} zaPN9ujHW`ukhZ)1+OiQaYjbpRJaag8rjloa&BCgmjZbsyRXsQTSBmc8hA8z;k$Fi} z!Bn)?YK)S5^4`?oc3G=mmK7^Wg)k|FWqipLuW-p3N^#yYT%CdxrrfcP+?Vl#C0ZJI z7Z`Me|Siif14wJJJ+gPC69&BA_bgzLF3_dJ3?v%xICwY; zN@WSk(Rn1ZpO4pi3Uo8^l=dGV@$%>k|7#n?B7X8$l;JVgR~+xH)W!?WWXDf8HTVC$ zBlHwOpysn>YHDh*e`Kqg2f%cEdE|7K?R?2sR7rcI6OOJM5Y!&D4*{p-kSXT>X3`+E zvUiUQ`Xcd%7%7+(u49(~Ef7H$4p2@0_&C?h#XTGXhNe^>*!%5|?)=Gd?}``j+=09H z-P_LEx$mB&RO{;$@HkscQ|R5~D91SktYg;(x`!h{Ged@Ny1`E+fUBa3UXF-UaX856 zIYz@K&(~jrb(?gy-F$qG^0mNnEH+$VkoC%B_wnhk8f-ZAI0di`gP+YPI$@Q&vwfyz zqJ$wJyoHF5XVF23DpbJD?t-m*rgH9Rutl7jtu;!kkfY~t+`>2-gHq?~5SOh%b2_+d z!Q3jPybk>Ti!~mmR+5jrj=u==z7d|XRZ5uo&cd38xl+ANd}w0#J)C+ZQA$t+kqR(F z&{)kG;Na?2-k3BV$)?TxHhWXVDDW|^*)A#6dlb7usSH+dac0i9iEs3^08#;rE~UeG z*NzIzoD*H{dxTuSzUB6WKlKLG6%g?fk}EPhCv6OMld(=yrakC7J$Cxd6F~LrB|2n z0%p;qK7S+vOdG&wd<~Tk#@}q*nM8`Ipln$uCN{)4iR&D_4U5=Yto)DPw*EV&`F+^J z>Tc~oQv$;`{W|mC>6%7?S||yNqtG{ZGPDpH&nigH1Af~Q5CxOEcNIY!c3vqL711}f zcnrN;E={d7)>yTQP6-eIaVF06@YSjFfBG}+{N7_(90;vf-gId;LRn~s`&h$c5k{Ez zq=0r9${$dfy)LrRPxHZ zT{45IMv;G!*|c{?30`S2fo>wEOiLB1AfqUn=BAeFQv!YQ@f<_Xvh+n{4jMs>dAyGn z`A*!kI@&M<^ngvK-~6;$gI`mlrv55M&nsOj;k$ckygp!BFXK?{Vuk zt(s`{inPCp+v79r!!^?FsG6;&K#0^RvFL^^n{;m2o3RMN23u|Uvm;Z(#tNVZpkv+T zwFJGNCjX|<_&9v`emRZ&jx^pqh1g?4JT-hwOp(}>?v3*x@JFY_FNQMD_MweehE@}O zOGp9@t~d87gCd^5%|-Du4h-hRg`*lQsSu3UAFSN(=Kt6CWA-c_CJmL&>8*a1ovJZeJ-axsVfuQ3zz$z$_nVr}nWwJn7CBOrtyR_xgT} zgoL2CQ-F{IxNVl%&ASlZIE%2uI#7$xAthw#bD=wWd3}%^K z32}5vZjq2km%pq2^H#xc{{z*J&#iwjp-fUBSP>MY)OQY5vR~umyZ?;oz)>u=lMOi3iRBb3!EnTGBqLpE*1d^D4`Y~SLod`J6osg#WJU%LU(tCE6 zaG&}6F+!?xJ^=?>RX!juv4&&aphtY1HpxJeCQuar9KUOsqUbz-8yBy+g$hO4@T%m) z#7qWhhOF%?HZI4a_orXpU5*(On(GK4uzWsd3jx*86j`scokOiC7xcR+5^{zPAxGrO z;7US?U8aAQIe%3J?8@>0SbxNYq9l!37nKZsm%~{5lM{xO6z`i0jyj#IIz>2y3f=G- zqmW6e)(#GeQovnP8534aFvwa~+?$T(}!l#$@*UM1fJvB{;UQj+BmPwS>5cAgRng+`ek z}6;%|T;S149bDh;m z&KEIBXl^C{Y=?*0t+>Y42lf% zkO_MMl|dH{4-@YBZ`<}A4-9qh%ZPB#Td2Mttt6ItZOiZXbb8OFyjA!bUr;6f?dSO% zD|AModxr;`hD;-5;$MB$Dfsfkk{M8LteRkCQfc*%zJ$0&fsWV!8r$fSjdrz+auiUybwcK)DP8&4y;$vPndl1j{qf2&u=B0pMe_e%j z*wP&{<27?BKeNSF>od7tkY%K@64+T~Y;s(x358~Dyb)S^!TK=LA`es$cqxWbOs)zi{nOVv=ddTOnu4Q+pMY>ZC*Ecyl3zi8|{MixpU3WnsW zCQ1X|)i*lyq|%iP2y9!Y^KGtI1ylSXJZ|#9#nB+p!#+;01`{<4xbLp#Y1?}^XyJ$4 zMhnZx`#WOxls2lsGbvl^6;*Kj5t>@C%oB6z6pvOS9(3IaNTyJVWQf_kJtQLoHaE?3 zdfd-bURf{+ash`I7gxve5SL3lG7tiq+B3ieTQ&KbZQQN!lQx$AK^ku39A$BJO*bCD z_9BbH0)iHE%1h4NxABNFjwqN}*E!U4B=01bPkyE5lCQa$rHLIXmLM|X0G5h2P0&U& z)_u&J?xOk^6Sa+#hr+Y^SCddm<3zX?MT0{nQouf0BH3SNZCiai(yc8vNYT`@zn4ir zuJhJ=Kd|@M z%F>HP>=ST!o4jpI>%QB{(-Hhs$@`d^UrG`5NyIiljXaHI`tvG>b1>^OOVIQAtqnr8 z4{_UTmgGGz@9aEil*GGnSO$OBYKZA`JMH7&>hLQ7s(>mOyJx@(`J^wgu^6M8M*n*x#1Q?VjHDgJONN_i2>TWu`%KL*zgL&0~GIYzeNs+fgI4WJCU&hN?A zFPbk*v2f?NAZF-_Z+7hCR8*cH9s(KrK%hLb>Pp%aRjYrAiVT3X%+hfUmjy<0AEn2! z%+GYlNRz1v<4XBO3S*26&ZPJ5GsT%5r(|3G%b$q??kz$uWcIxgjmRXB_+dl;K2c2m zUcdqybW2B37iYd88>!3~l_+yt;q{tk#Z+TpVf0N@jkgQ7t2fwY%*A3_YEhYY4EOpZ zy%0e%<3RRA+vgLeo|u)fAn-nCT&5y@0TIO*8EZxOnn|!inbq8A#5E4F4XO`ke6cyv z?)<4xI6+Q5(T9=bEA`wq^|g-o>sy&~En&GPWdTGuhJeVDX!bZ#e|8a43Lue##2 z;@Ya$1%m~amchT+Rd>a8>o+@kARgA7YdI4{@Lb?P)E;9CI05Kp@4eO9eYJtAcexu# zImc`@AVc`EZH>M2G|~IfgX(|wp67qqVCPt~R>iT^`aG@Bb-iT&9{Y~t`2*cy6?$_! zU%U zXoxoto+hOsz$J-gtlbpdZ?!hy@wdBbhY{x;M@gIyJ zn)WFTAz*+)DW!=a9`tIPGAnu1k|*m?SOz6eOu4_)wGD$(y7A^Oh6gz2;o~Jpiw#oY zjE$CWPg&XNO)Mc0L%5t-av)IFbZ7?HNKV<`2gdsBM^Gy}Mr0h=bzX7YyzlK5I9~_C z5y$sDgyKl6{hDt6*d_ZuG$dA(*uUup}bk_uuo*@C(N?ml4 zry>N2%LY;dcMoMq0XGrc>rLg3Twg{}1|y8ssj9L4^7mBp`5P>_DcI;NdZizl>!o7! ztSkW{()SZnx~EfbF0S+=9knqjWIPg6EWxMQh-AyIa>2D5r%PX7Qe}(%qJ%!D65ApG zqmuNuRYKwPq9Tcy28k@vr<(GoOhEQ0!SL5`9;8?>&SlwPl+gh?sg4ibmBk^?fnAzM z2LM%`cOy&(iO$8^u7pBn3yA6Iiub7p0&{5{oH!*K+PDE83J@GwF*-PZx4(Qtch!Z? zKU?l(WJXn5sj5V8Cv)6pSFpF|%WAyIMxc!ApSs`9#EihG5-lwtj|~bO|H{ML&b%5F zss~?lSc|ZbOjZacoroAvwOdL6)$54S=T#K=ho_umX5U7*aSpU-6VmoW&rVo{WC_1O zLWfDl>YPIHN@}OtH3A`Ie`HgsRg1)thKzykvf5m&8Uf1UCrxhKzxkstlWA!0nX@U4$3;6D4>^(i3kc(r_J; zAisD?FX*fh&U2(Eoj85Zle!XqI|{f<)Tq8m8@sahSE!P=tD9FP%to7LM3`j<8 zt&172Ey;mb6Y|H>&Ou z6&YR4YblHuW1nYjb%;@qy*EEB4DpZtkT3tx%k&zP(WH069y*v2O=;=JIrQCGC?(sd zU3_3eVhmklg6+mUvJyVe_3OuxxE0`z)+_;vF~WV#fu7eXxpp2cu&;0T(i{cA6CR$e zz~&3zF~)jaF*(!z>+GwPjn_#yQT9{8)!4jQcPcTYg$G$4*GX5^n{8GUIDEDGxIOtH zcP%^C3p!ea5l-EpaB3QQMg87hk?y&ei^1IfVD6s=nII;baFn#|Qrr@ew*@iX0T-R&jtQIt*>^A^aP6Ujh}* zje$lKY|~if+eyz=_90L zy0A|{faJYki=Pi$XD_jS$Nn7|!i_ose(*Z@8Z7R?)Pu|FebyCN*vceznKZ z$0D=}K@ANDjD0)Q^9=yAA}4pAE7Lxg-}Ah7Bd9;tTYWG5|Ba;i{Dtx+dJJ4PMCLmJ zW63}F!a5w<-I$J#aCowHMta}+P6UEB96Zo3*xN#kxnMo%`NA7`gh7MofE7{zg4l3A z8ZTE{rb29;&Gy6fQ_o*_@PMMe{nr@WJ}USKPW-BTj)Q5_fpGo@IlV9Qael$@AREu- z%BROz`B>fV{8Q155(70OE?gA;1eQ|mt^7?-0$0aFta)U2)zHK)K7Rfb%pIe`6x1b;f>0S&!<-T=vX9ts zLrtccFD1&!-D|>zEd2SnSZ`Mrmm}kT-dBw6qd%3hn#qsmOgt((zJnMlq?DIFi{gr}hEU*&tBj-}&eV#z`nA;Zb^UcAJuwoQ zjj?mSkf`-p<3+`YPOK%tQJHHq{a^8b5&O8-nn;oqfouLcv1#50+?)C1@mi?Vb@OGJ zJ@Yo~_klH0EtW}c0TxZ^?OYKhM$n=8RqKP;a@4r~gOcO2E+lim-~6FW(^htI`f&fF z`eYf;v=uz>C~}g6JkKr3Dc`=!%+>^GvbE!FLW2$y2XFms_9Ba&6P9;1e~2Lo_OVoi zrYOCCcxaPss20b3YYcCH)b`=Rsp!Q1wV#m+B|1+#n*>PFZ#sTt$ZhLg7x@N7G8H%devTj;NdgJUN#)*@xoc0Sw#yn7+aU<&Yj_wBRgOl8c)yW%$ z+8K^8P}1TDiQ>C$91G{%39a#}(EZ;tciIgmBq=H$V44G%lEh zec0_jZM2sK&WWvr;-cd+6q&ZctnU2)jO4w+`M+=AZ|VgeoDEzNVb((AwMWDCI-Pai z_-?nVUPBedYP3A(ZrrZty~fHAJ;BBeHFij#BV} zxoQ6_U}9$y(a4Xnpj8xtFp9E+@pz_OC0m1`jYTf}!U7i?E!p$Pcz0%YWDO{s^?PMQ z<^QwG^fCDg=DKd}{EPGZNAj7J$KUPsGW&Uodi|E;?tdGFD&UL1xxXy8!7DExTj#lg z-ryXgIQ{+GVRjNPq0vv(k}Zokr_x4cTl0%ooHxGN??`${VA=%DzSD)h`{&eVOU+EQY5B#9j82lqOqSHnf8bo4`+M?H2Mg z%}rFyi2{o@Nm_lkS3SB2dQL4qJpUz*x($l590Z?KO|`*E<|ZDcM)8a=!4e-cO-20k zAB3;z9-$f2Fl4Y)UWRZNeBD{WiDC#rmd1%dh-|P(2g^iEozl!FZeoTf1{<%Ov$$Gb zV?JUV1%#G3BwQ##jxLEIhQkJ1j2xNEJ!ML1*`g3MxaGPFg4Qf2Y`lz__uaUE&XP}* zv3(SAOD>-&|I!12Hd?@<4S1OhP8eky?ME%jGvgEyBnnDtD8)V^CUn-4XYK-QIYB9i z4m1W^vq?!PeOAq^J1E`o1!6Y^f!!ohe;~%>+U8gRCJebG(yEiD)=wu>vWJs*IvSmg z;%j{mh3~Qki6McD^H zUu=&mZIuMakrI~4xki{rHNEji_aZJ;7ZTTAt?7g@eAWESbiXr?GpxUkE4EvX8)a=4 zzVO$WqjWh#GMvztIVh-wf_*h;u%my*aEUVdFX=;!quqQn2rI_*C!bjFo&K42>t$K; zLMs@vX{IAMu)um-$7ggCwEHZ$izYgonWau*51wZvjKOb=oe|=$CM0EF_<5r@7TMT5~1-RBVWIB-CUw5BQmPJ%8LYr7#FqrY(|sGK(Dd=J4ab*nKt^lTRz5BbaIktq9|4|9-qbSP77z=L_>0p~xxQ!x56;JcO65H+RsR+~Hi~OJ$6xrJ0n6xqrXSSy0FlOcFd(32z3H^~ zqT6Oo!UP3h_00jLH$m?mWA&cZ&B#{>qJDMM>f7F(r$5FPbIpH<3&c#?AYvWRbJ3xUqCU>O zf-|>tfh5w1Rg1=zsE;@)hzxZwp&L$56Z9!*=eLIY0j- zDF{4*Me+;Ok3)sHS~r&c=R*^FO%@@RL~{iF{MP_>KwoyYvg!Es0XviwwqF@E_zWUg z`*ul1tnwM{}pXtp{ssO8O^ha2|AmP@u*rlQLf>4_$dm4sknK(o-(ME_t z4)5zS0T6{5K1yvT6d8N z4hm5m>;C_G0Y?0++P$>>XA#ylPb37siN5!n0{UR-BQS8dZ+z(*z8Yg~M1GTdEkFt* zU0rI`Ui4D`0Ot`M3T8x1{TM zoadn+*(RyUCNf}Ul!J4`xYIDKt^Ud`X`9C(xsyAlq&Z)lN|HK+7NS1slofBFy`Fs` zQI1ZUfwdEb&SrdbqLEY{kyRe+>VN}7lV?>hfhtoTtsBApICUH&^6Q?>%Y>7zh+DF@ zK+%h&Pcct(3tK~nrU492>1yA$4#mOLoOLFiM#6VpS%g)I-bR+Mi;wI%QQGxJ*R4Z~ z!sf>oq1yfn@8&d}^ytjQ^c-E*=wn=gF?mP7LE1oH|dGRRofjUg=r&B{;nGzfRdW?y;0h8-*W-ew*Gcf}Rx@%&J-A zle=0t40}wKQaG2ixegiQRQV}cA@R7!jjs3bma;8T6kF?hN z)qs5s4GNPC)-08D>VINu-F=mV-<0gbtTrCk=at|uSBF<&pic;WRFXETm(|lAN-uT; zt<@b3Bd&4T_fGE^y11dC9FQliQFyX)%BS-;(;-Zvbak4FOi z2VbV)tG>cW#G!AS`1kM!>YvXIPkeuls4JsB`*&g?fUub5P)x+m?6I*87Q0#y;m{sK z;T(l`<#a$eiEv?#hTI@(G|WJRKhfRY-PphM>a53*?M~>_+gkuy&JU;RB%j0jFERSw zCvV&TN=aU4KR*=SaX!}scdyTR{ohb|tQZL|!96$K>w$!D(D*PZ7?ch+CWOj<6<){P zX>EOcj+m70eC7N`%KT{GQvgehW5J3$*eB+RsJ|rAc@FZMEk&gi@JNU6;n1%9fxlg^ zisJl2!ckCAf=S7Or%RK9uGUppR0M8^FcU?^4g_+_5KqFCizdA~uIQpiMW&&p;XHfE z@6u}~mM!6a*^JHkm|&RBuqh;uS>z}exZdTg^+)4p=nkH5FEZ6AkU3he{bGHgkk+J2 z#mxTlzzuP~A~2WTjc+>MBP!|_YCvu9QpcbPNM#Z4SWG2=X=^1o#M4QZrk$84pb1D; z2nmnCJ)>aiW5qfR6UVS08xG%&E~KJHcBRbnh|m@Rvc!6bm&7nlnSb}V%LoF*Rd^OB z)1<2er*D>AI75Je20Dzx))(2@^w=YMi`2-=*4l~fCle`jwi|lwPLwdqBho z_iL7`)y%jQJp{{^%Ma_h`kp7muLMKEbcC2n0dmP~LfylfS)}J7-@0eZY)avzS8;FG z2+8vWZr1C&tdLh~b(5CR`W6)gud>02+xzXL-{iyaNMMisW2_6;;S8cIMF%>Jk;ySF zx4cd7J$Fn+nM)TyL=cn4*e@TdJ zkh)TDi6#P_UDa(3UwbE1T&d-@2?>9>&@S~j(~nMz zdNWT98SLe_{z@kVA@E4ych*hfwd--??9z4?Fbq{j9p@2db~~rC((}A62$sop{+)69 zY;?D_k1=9w|E-|={0PeXlirF%XEI`$PwxWn?vE?#EF5h`#$LuI4so?~;%yd|~tWBb)*F?T*;Y$7CU z(TJjUBbMpcLz7XN%uCW_TiuJpfoK^j5+_a1Wf(|yFHuv!L${613^C74DQ)h)e@Wu9&1CXXJ98q`i|lL%-%FSFu1WXIL|#&q z-Pf#?Cfp6jTp#|_dD1Z&iEV4*Ol(f< zq+{E*HL-0Q6LT`LCx5T??SJoY*FhchVO>@IJnzk`r*P9eD3YnS)ZaOZ zu-@otSTOgH`9c9nbuta3N0PY$9wL4xDrPLLwY=w|BvhXe|3)a6+Q=3M*kkaK;!uD7bt?xk3V#N6k_?&v;!7c5O)c8Ezi<$&)XiF zQ{cUOpEouQiessz+BH(v) z{*n!T6;kZcd32#f3%0F?yfGG5Qi^QntK;txRdh}eeOL7#1_c|UnEA_d9l zxB6+$)N$POmE>Ii^9buJOddo9z8y+o_`=gr^il$d^CGwbjZhuuAwIuc(=EN0Xl*GB zs*yjsAB6qDs+755bo7&H#0f+3!&H2LE2&FXiUaDmCY}Z{9dmXh2GuA_-VebEe-FjR znlQ05Yo6<$rJLbki{P1Pu3IT{qZ*ej)mXd@aPTFeO>DhQZ4XqA25i^&kPg#VIaeMz zs4Fp3Kr*b5N|51oVFCdSKaw%b=f19$eTytFNsPQDf>dweD5rTA>$887=acI&XW}`y z`|6B48wmv&3w?KGLHT9sTXJ9|LYNTy73={F9&NGTaymIjq|9@LLwCVg7xD;Bna?Q+YLNtO)0;n>xfL~gXqjcg&x?3jmXvQgaN??N{j9rh4v z$b!*c;A7jD>}?`_aOTp7igBJ4YOJm@x4)X0WKq1H5>Ciu z;Am5RcB>Ua`+K(xMuXl+0Rp50t+-f$$X5%D^R)w9Pfw_w%BCO;$qSt13;qIrg{%Uc z>9OfcsgAvGc@JW2o=0#Pt&R`a>)lS~IZ}e@O{<8a})i%>bWM(hT zgwfD8ooVXJ1D-4bO`R|R{F*6*kH|4ln~z zbJ0XQeJUCdBBrz}N;d}z%JRN>#EyOvd)<&D$n|s136mH2A)7vl@(_8bL7QfEqS$dA zBgWhFZFV%0JOYK0WRv=88C#~^=I%#DU%hbMq?Re<;9qD^&}@t-xaH@F4m7=&<4<_O zWXJys7v*Hg>pXoNx14AGYh?XP7_xJxNngDL=2K1>xV(YX5& zcMSJnQ2*Zo)I(FpeA?b)n7t1QN53h9J(u>wza7fHFaK!_{r1qPoZEut#Kl|yZ*R91 zrSiJ+X1+i;jB08{;KKDm4;d@5+y$y6=kudu3lY`eL0U}Hdmy){tD1Z#<(6I%BE)-) zk>33@vs=grn5@^p|{OrrQrSqd^z#?alD zRZ1#f?#*pBSQ3{|L+n?KWY%(PqUx6u+q~Zk$9%SOt^cAVbld8M6e~I(V1Ue z)&+PFik=YmHD%>NZPFg?R2#-7=W?B$r#U^VRg+%hnjKA5tDDc?Wk->k)fN|aZP%(M zL08pu&GcQYjO6Mss?+fwcPbzNiX2-S=$$^IaQKPYLH;dij3gijNbGafh#LGI#}`yB z?Q=uoeMW{>JH$#zM5HT?fM$K9$fowXx6o_hbKN7&FF!j?CaahE4K)u2+Gkx$gH9*Y z3)*e96Z|z`z+(=uZ?jecm)YUoi-d&avrXk z3n)*An0<4n7{#sCpBE|lBMCrqWp*vJQ*+TYN_S?-7AzQcJI(I{R@ziXE%Fn$&>URU z8enk3E85Ftgkn}(*Z>Yr3C%TRi|~n+;xZemU&G?4idLBBWX<`JiQ+(tMpu)mi8anC zjybFZXKo#h=)NbKM@@6(Q3%MO<8_QO;tfUED%%Mc4vxh9RmH$S+@mDJ zda~?T+YCHsI|RrM5T6^U3>!}ql(C^oFocWa;^KmCw(Zb9#JUbt2sV@27m;WZhZX>! zmoU3O`3WNqDe~+rbH?VeEov=#0fQ+IM%FubvTK2+!l;Zn2H}5_C5o($#Cp`ME=8uVO z_Nne>F_7SjQ>9U)3{t%I-+SN>qA+BV#@LfRN4}?J&!q&wr+IATdAP^cGz`KUv7%1g zltp(*>CeM6{PzbP(jrufqdEfs;c>611uGa~kW9^L0zlz;gMBbi28e_@FMSo)C@j8Z z8~~0Z+TRdi_FxG_`vVR>Aer-L!NIvjyj{APfWZy9Q5s_*ce_Q{?{H>p?{>8QrA&Jt zne@-|)-^UU?t$u|;nD3M9|v#mpI$G?%AXOo#A7@rhx>#F6_aQfLlD5D%nb~_HPEy9 z8dm-ow!L|6lK%EN&+_kp<#X2YYqWJtIF@zz>8H4&Ik`)s(099+m?6|1DYwV#KN%7J zB;K=gd?)-zGIF024$q1Cu+{aPgj1&$u&N}AvQl+>*j5$rg%M>;4#{eb#%Z^5hh)2^L0PCm#GW+1k-sia{am-XvTBeDogt<27 zaN&KCQEi@fOferq*uY|3jr`TjLUR!$#h+~1gj};2k#kI%U#n_97WV4(nH+MiG^3NJ z>?_R|wijtJlXPR{-l|jeTy7)ru}9j%wk4Ucs$BV{d0k0MBccbCmt+j$_2id%MIxxO zmbZV3hm;=aCAGAjn^I%vJn>*&WLDhag@0qk_)e3>8q=gWz@#_L&V-0J(iivLO@rO( zWFAlsfd><+xxivuqEy%8HCq|Bg)R&l<-MuIN-tyd9E&f>=h5~I7VV_#_7lMbKIna6 zOXRkVqxbiaZT``zv7yZ2yS;TnrVOPxkQQw4a}#2^K&aC4wp0{1|Bu1$(qan%TW<|h z{rjXV0xlbspX=o6Ey(6zY8Kw$t{;q0=B~Da!x-SKajy2Bbc3VihpCSkx;;=&{n95V!$;Q{#~4eYNMcO0Y}55DixfO3l;5`nbxd^D)R&gh1Q{>k6&@y% zQuIy2u(Wjk=LUs<#7c}G%SMh~UVNechDI6+=Aw}RyFCPUjZ9Wx4JMVaweQoSj|f4y zx9C~F-pSJ2{W*O~{QlD|o_h@FbHxoy!%H-O<+M0p->_pGfTXw=US0oax%a4P^j5dF z>(Mm?RR9%!{RC;ePpv%MXyW+}{q-o+zTGL{eM91Vz+ws^efDG(2LN}K8H)^b_T1$# z65`|Qbvu7}aBJS!4*2@qdn9^xT)<%Cu_uAHfbL>_$wf$aI>!0;od0Fn_fBKEd}X%n zsSsy6Jj*A?=X?Fn=Y08NVj^-yZ5g-9GZ_^vyPEwFm9WBjS5Y?_=+zw?IKHKoV07Vd zo5pS*nBFA0`L9!VL=(cy+1<(Z;c;7aprEaj~AuP$(E?tPvnjyz9 z>y_E+RBV;P@vj&&Ay>`oF*R}{9Q7$_mZE@B>0~KjN0_f=2@hoP@HlJiJx4XP5H~!w~=G>(4xQ;F($So1$a~HnDm>pq>Fo#BE5@o zHGO$Mmi4ZmgQ@Z&M@Jx`wp!2Mp4e?DXjD_60(wLzXQJxNvDkOgeD!7qnSlgsOFYqR z=kdIQHC{Nlxz61?PVtKa2)T%v7QIJtqwaApn}JTHtfUjPG9tY;@KsZFAIgQ1^IDg* zRNLLxg8j@Ps%sr%{4?2an6_XX&WaORk=gBMtQ{e>;i#Qw0&hD z!?kLSc{#nn*HpS=q%%LuagHl7fD(VJR3M@Id6r9}B)uAE$s^MBySotrRe5o^!>^0H zhDP)bIF$KUfm!@N?#Pq=kxlPn-Q?7qnrqSINugWA5a#6l=!LL=_#kyNM?e!?P%Q0y zoaIoJ`>NZ#DU~xUq6cS3G^2fVnPq_9|NjdH3}#>l>OS}8;Y|a4pv4GD={qSJBA)kQ zPgx(FLUHE-kXO&y+}&;g*93+xTkU?=@_k3>|L(4^`tAhsU)xcH-YQ(?J3rOi+lQ`wA1`~4+TQnkPlW`&L$?t&3Wq)S?nLSb0#U*F)}Xh&LOr*;-i^Li zHjI9mp^9Q+^7%?{q;2w@_JjN%D85H1!hTc7g?mdD(PS-xh~lShUHwC-?*?7PzY0H> zaCTP8>+;-o8piy6+?+|ok4)Nkif#(5aA`d*Y-m+M8Po@i{auN&Kn7BL+F>9z0PV`N)i{{g|>tNBd7S5TyuBhI+1CJvP%7-9z51a%> zY&=@;wAM@s45?Z3%2o-0j2Is)FDlm-?rjTVb|RCj@-Xg=A)*=PdhQTld8kF z=?2@ZHIAsrZ6VwHY$&6a^+*MyLz)qb)c=SX6TtKe>NW=M@C^vkF|97_cHBhW9=4NGTFbM4;-;JHXD~a$Rg&_zeDX>OSwpK-@V-*s2qF#9D6qzghqY zFVscr8tZrm3i+CYS{eqf-C$`7ceCe*k=|{5aH-T?tgKy~vP*uAtkkNSs@g~?N>x$` z$~ss&#Lq$e7;y(cbVsXL5CE*&|IgZvW8I6>dd&VlEw%QC2K!t;lsb>eLIhj z7d>eytb-KLR!GImDx#a~Iw0QGXBSi0iXTv{m=ANU>-QdhVQ%3=cm5)rmnYb4!<3XJ zm&SV`u9`f<*pap_{>7iAK&BTWRG|03laKF0%qQ=pNvD)2Bnta|Rcds{s_@KgFu4|E1i${c@R#hcD8W)J>@*`y zv?OhAZZ6tF7y?fBGl*^^2>%TtW&b1Drlw(ZVPJZ24u64a$FY{BHd}3uEXb+=>sRI4 zLm*gc&g@eF$QolyYV8*G2MwSZgO4qwu$Z|PW7nNxJ5=m+{slWipi}ME{M*cQ6+>v1 zBhAIB2wP!8pL*H5>KI^v?kP`M6`+?ypw+;*5(1?Q(_Dhx zH(kdS_9GpbA(_N`KRhpO$djL5@UK;5Imr`S)?fjV70phf%p)x+)6hW0Ba-f?#p-X) zr7%98tst@`6j?Fvez3wa4h{j=Lz!610h*_g2lx&PJ)$e+r`6{g7wVJSPDY{C!<^(T z(UKD4AXDt}Z@SNh^rIj-Q}YJzsTi!^o>9l$5i25#&&HH#4B*+1#j})?32kA9XHV_H z;Af$d{K$mAumCxCd&%s4QATO`6;eidwq(Y!0~xj05~mB4YRm#(lyeDg_ODS{jGV2i z*k$86Rf}o}vDgmD$fU!`#X?dRG&C<~`VF}U?Muj}?~Rm=wfeLy@q~>ra7YVU!ju2& zbEV)s56RlsZ$u)ury@dAhY)HRLE<$%bn*_iK!6c^8qZCSH^hJ1y9EWIZ0)!)ICV&0 z2EtN*2WQ@i)ZTr~Liy~f{<}4XhM;M~TlUn~<#GN|mZ+ zxnCu(cJ-HXN6Ad}Zmug(dClv2`EC-B%PDlu%pBaYv+kQ9G#$Pz&AvMh^H9!DUq4BrsQ&2$P@`vgI z{~ew|K(1u*MKBQpJ#3Rk_CND)gqu%-J~~O(UWJvcvDrP=uj*uWVfUPWyx@zocmO(? zUx=)X=HE)O_zLA_!-gJCQ+hRdyPePh4HgghFnofAce;h~$#lYpHYrO+;{DN3Zo|!; z?c#Y;hI*T+Knx|C1YW)fwa-JTbl`WnRm&5xlbaTF3lGNe;W$h<;m5+X7Ea1yI?NzC zZwFacY443TTNvhaGj;vDKVV0RIU;FAVWh`)94K+-uC3=rg|9U+^hato7KTYG$Yt@m zdzJ#N|7(tY35)hn0QPgz3+zA|3+4SBvs2PeMe|=kvO%^yfAp?XqFjT|H~rZ^J{t=H zyFYKD$%A`&s{ zZP#T~I1pfLgY7s^iN(*?bsG@_Xnrf%y>DB4x2tQtPpMz~Bp;tf?+;%@S>c(u%x|OZ zxMF3jKy^6Hmp&WO_fO)~15pV!{2zVv;2C3{c z^_8TuvYU6e?ddRBa;r^d&D|uWgQVD)V}(pfigUdUKS0<8^Rcp$Qup(m&+a|&3{-nV zO3pV%geYV`udu|K^&*e2HuBP`U}w!$t%X%pndI5#a=qyui;#l%T~3lg)o#~DJ%#d6 zqHudx^)S;qq=Qdco}Q}137ko~Naw@-`$YrgR@X)yQGw15QKK%6U3uK4?2xI#b$9b) zSISu58IGkT@f^|#H>Tgt#cOyN@*}8Z@o>T<(=+*c| zmq%NiORt=N(l>=ur%MNHGYzGnOI3pzk+x0mC#FVSIZAZ%h6g5FdHOtv1l)mY?Rw-P z((?`t?Ca#pXtVsHw@9Oi?wB>PY-|oj$(GCzq>A?4yKn;~Mu) zzE(9MR~~DB3s5@F-9+AmNKb$Iz%Br*(fG#LHRBYF*}kjc`c5oKHbvZ*sNoR@A*zsO z-Ok!#M1JRt@gD%lj$^$rkDgR1X0mokgZ@33z^^e4F~qi`Zzf&-t-R2x;yTw+qxZqC#5~ z+|0@3mFd{>urTE1bpKj9#)TpZZ+^s$Rp!}5@OIr4ZAT{YE){RO>9&BO>P~RGZ{O91 zPGMpeU^-7*pco}rTUabu3U&9YJvESLeLmvN#r?lxNC^qT z2|k`Ii+;4funkc9hgTKCR{{6}B`v7N-aUThX2_#j0A`boa`&aP^=FcC|BtDlr z+MQiS4Fi(lB>RHoBLIaCEBqXG4>Dest|wTFBEXAc7aZE`Z%)Xoz)#{@X+jnkFaYF% zxrQlq(ab`apJ%5DJel350nPz{_hy5uecSCk8{}2=^q<;t=)0!cIbR}q<@x$J40!t* zhHc+@EVooVG{eo1&z2YMEzR{bfn10PgBFuWtQs1;3IqjfqFp@@YB@nR%=vMcAIRIy zjIYpp)lKw{>qu$bBzZ^eZp<}5Me9O^05{uGqm==OL!^AgSR_K9W9L33SWJavrS;K$aqP2}429c<9QyY`p6Trltz9_zhV~@z@#mg1wCU`?GG_i;H82IIk+2g=fet`qy;!Q zhe_wCak$o8clI0+CWNy=S~mQ&iM>~i|H55%D#U>Vodq`8q2@>fZ;PsFsz3dDjbT%V zw}VV#rqQ(!66hd#WXi10y`n+?x4lX7SXkNNvB?(O%Mgg#TBE6;ka6ym0TB)M>3e_8 z_{F%;EcLJdqM+W%!9mMrJ7K#m3=3-V#%jTYDY(SGxV67!TVKY`x}!``GkdYqQ_{<4 zEiUcIZ7KDS6l8>RqlfNUIFkx!Z=dIkWz024hKrQXyBqccVBlHQNsUcMdqww+nCq!1 z;3hCZi*QIt{pop*5j6@L4I~E?Os`A^8p0Ym=$_`#-v1MhLAU68-Zvi$KCn8yaGZ17V(My2Act4p%_yx%+G9=Qe3Z;+`Ad*KINKpXg(LY1L zYYdTqlN}`A9vw*J9SA%7p0ZC|00!(AvFe5x=t=18i?JjfC6z0KNTz^~8yBdou+|6o z0grswU{14eSrcqMwpjY!cmGp*WaPYtv(t%3{B{)5_n|EOklpvVHFR!9)K%Am3$eM1 zh9Xs(IDUI8n4S|Byx}@^d&HK|*I3>|3FQTuP{)|3kUVh)hbl#oL195$loL+yMFozQ zLlH+R6>}(gS!D6^X5_#aV;0EGYX}X1p$<3u<;Me9zj@AK?$P*`P?c47-if@A>fM8v zr(VRgxa1{R8rkrhLN6mT-~TNX*tW%l*0T@gIc@+x+p)A%eza`3#qIL)S(eSr?z)!i zbz?^OAIHIri>SQy3o5s&Ysjl+Qc*&OiuU7{o+y=ss|cvYXLg#@RJCn}(ggJ$SKSn; zc;tIhxM@rR8Xo^mPX{JVmqZ~h_^t8AU$ol2s>vw&AQ1%#KVays@!m@^1^5W=gaEly z%wZ`csigRUf4E6Pnc}CatSH?|j!l`CDAtn-M^Kzt9-i5vq>y0HxPTIynn=ytr6g*l zS5X|zeEn>P+eu>PMhEric%m3=qSu*Ee-O=$9Gl_DS|pE5sw+65^rHy6G_x-~d!eGH zb;)8oc{_Be55=O4;*K=|i5RI(*qEDMBuP7K>zO_oDN(D`mIIJBw3jtkCoNfy@F=uA zy(9?IDT+1Q>T-f_%^f{jim=!bAK=74wseF~GdjGBA$UoKX_bBt6#fVdKjAKa0ktuZ$;z25SMmA=`<}AKc z6l6*@NR$AyQ}|Hd%p;9Y4oQ)ywHgtJ(lDnJ@xJ8>ur(XbCPgPa%bB;rRlO?;)QOYw z+{#S&?}7SO9L}_f=NX2VW$4qCd2cZM{2IYjC`x?&%rqvd+%VG`g&Rw>^ds6|Erunv*%QEMq^H14N*> zv_$rG8jx2i?)JqBVgJGWM#PfFhn&B+!F6`WWs0e|%dH#Lre>RuCL5a^Tp8yVS5pX= zgi;&~AVHq=M@hH|s?FIl>cSl6?{O~}{CIFyNh!<#D6_x2Be(-rTT*KjnYMJl`-fwj zTHFh5v&s-c_Ccbn?`|#Dn(7H$)hGrM5-FMNqZqbMT?Dv5`~XJ6RO|v(>XP0#AC{Xe z=XsD6BY{#m8Y+SkI!_H7L9Qvq_B$HJ)Dth)1?N$US&9xQf9(fJ(uHT|9Oo1m$HWrp za8?9|w&OU0wFcU*yEAi}fg{chE4A1)8{w$vhpz~3tp6!@U?Mh1c_BV87HU6Eq8mHc zK8lC`9N4rnh6kY#Z>9Y+nTW<&?gPZ0E4%>J*rQE!Kd2MPPW#4w(H%kO=}wNk7uzf; z#upbS!4K604_n#RLSMYO9B?NO2rchA_%``dA20`4J0usQp`G z#;Mh1jtWcJb&avlXRf6vCkN88BX=>vYEwR+C)RqsB*VFvh2>hGnabo~&SHL!EV*M0 zY;E7prB!1iz8ty}Luvva#Yo0r8%~KHsSR7}>b^Sm`=yX^dw5e#)aK_cXCd-IR zDK--KHrPqqh^nlX8fawgT`W2YE@lL8RbVh&0BdS$B(6_{a#f;$nEg&^c1t(wi%E;6 zT=P9Qck>j6{WWrS*H6h{s*R`3DHaj15l-j1;a+lfw>{0IZgAc;YreKT zSwH6Ef>{>hU1Mzsj6Tqg7MXc^C5yMCs?D~YHo;fl`Xy-H$dA3mUC7b%r*}q-Prwfz z&dj`w zW6XqX`pmLsECh*^IDSP%SSxQG3uwinp-E0j5kJ;S^sTf+RkMN^Qr#uH|InYmQGZH_ zBz|iQojY{+`9FN9C78=XI0Xx~t1!{t*#5ZBO^@rhPCCpS!tWvvq~L`#vRnv!;BqH9 z6xn^JkSH(C%JBU`2B4IO(UyI0V-_ufD3~;y)0{oK@1(;o;XHl6LB-LR)kdz4S5GJ( z3paje`CqdnLO+lDUVXk+^bI>G)_AsdWc&6^Pc!1Y)}4&UAb^!WEKbRt%krlzSIF&( zB{10qWXhtZdUG?`^5*bw%`G%bSuO3Fj3RIu+1c8`E3}H+ESRRJi&#N1tabMKvg`%; z(Z84_bQjJ1-a72{7YZI^?1FGzKCeN@rr(}?v>1bP&M@z|^WcmHg+*BeiNDTfsufnO zBS>f=(3dENC1y*aS`+wv@1!y?M$F(>gQc#zSnbfIQ7b+vT~3uD_GF%K!)d)eST)tf z+Gb7vX!nnWdF?3g7XJa7EPc)^s&%eB3Ei<)_gf#;DAC_3k@E05FtYpOjkt+SIH)_S#(A)cpZULNeiY~x(R?o7MiEB#)Qa?nC zp{A4u__+Pxu~n2rmc|z9jf+%f%&aS!Ej61@A&E$%FTKgxe!^G<<%RNL^RAP@tuhmR zAUVmC7Cuer@b*RpoHQZyqXA{hN z%wY*rL0wu!-i6`0CVU$c+sk(9wS)T15|=ttz^E&=R-jC@AF$b=IfCY-lOo6<`6IGp zzK#-(cSP3t$6T$op)lkm)wKT#o%$RXC3Ol4`hZFZw|Pg8lvJPGaKm`)Q3g%PL-;A1 zm6PTl!5lkF+aO%3Y{$l|qaUN-Y||;KPclfcFhxKakMCKNnnD5i3tgcEr=vf10Hjb- zq+#VMa#+>!Sml0YM8&GG>eTgiFp1Q_+!(C_tcXyXJhGmBARx&W9Tx=VqJ0k+#UE znb@OHib6${gjx;S)Ph|B``@Wy++ZoyR!8-x7*_exvuGW|<-^Jq>G*sk>=>!pb)n)? zlhb&uNy~uDdf49on3Tcyl!v=Qast=e=GNR(Ec2T`S%A9k3 z7dgZsFo2w7>`+<^f~v(yFcUUJ4TNAh*+(XGD=%*sym@-=eeN-ONW%H(L-D_|40y5> zesla%#K`+b#7(m$FxLRp-)PwqlqW=)u8-k zcDP1%ki`)9j9JAxeTWm{t!f;&BCR>lVwMV5anqcd@;0+v75 z!6k6Lek6Giw-8Z1|7}ehvpSd&3gt367(cnn(Qack%4B7KxX3&{des9plE>(elfnfdf6$YL(+-_4_cxN9MO?>_-W8q{^7@@J52wj%b zYY$Z0Y^RfI%?U{~_vuM_k^fXRqCwS3?2Y8wg9JTVB#a#8QBa||**e$q_&5Vp&2m<* z_Y+nfV`0b5$!J=J_sOOzgUVt(+nk%SL}K%reHiP9<3$dcHkSgs8pk@e`Z?=X4osAD z4FP=tKKQJ32WJ(w{Y(=ZfryelE~s*h!R0gcuC>@g)dDV)>a?s8JH&l_^%(CB6JoL> zfoDm$>ki-Apjo|)R)xZAzj_xK=u2-+ULV}X*^algkuDz`8P35zzifeE)g|XzXCc9M zd&x|3{oBDUsNho46l+OB!0Lr#pLE1d-#l0{?M)31Df)u^F3PeevAJXULDDa58spve z_pBxHVZK7bC8w*q)U=0X8VDc6K`(e|jk?bG?J6{9F2Jt8byeSdoQfYV-0Xx}tB zCSWBJ90SkV=V+RXQqmsk+{#V*Rct2^+1q6aYn(+9c50~8HLo`=G8f7#%WvvH7r}`^ zOkA$KEDS;I?7YZZ;rgM5YQlH#|B7uuW9DTGN-+i)V*>QsljiLo0Rw#J*u^8D!ujQZ z%10grGZJ*b#P$bdOS=K=2T}u#qzDN^4-(!*fo3LaffT&lq0q=qbSw`}5;HSs`G?e8 zDq4T=0Grnydq^P8rpqHX^QQZL7KF04uI0ULkNx{38MMdqZ|2*5*$)=Sjf-o0pOqW~ zq75gKqh_tErC?fS=IJuzcsyT?W}q+bjSe>Q3&Kj10-Jcs)}U|+$Ko;O*Wp7xgFQvL zWFN~)o;Ye{0~Y0_jS=nryQ8Ey2II@H0JCT!yF4O8&~M)`ZSHyDO()?7d;&oHAnxJB zgzVmP*Ul*4^T(;MQ403WCx)^hnTKySxyTGT`jiXqPfnVFxT%_dW)qq!bM&Qa_Kmoc zrP><5b2KGPG)$SK;OwYI7!($WGnX5A=Z);6kys*?li+e3j`0+Q+wRV(Gpvf4B{Z2; zTF=+O&Ll+_vx+F$P+i(p-UL@K2u&${0J;py7yP%FA?!A-;mI8E(^+J*FdXFaZ+fh_ z+7`t_k1lcKxkEMhbB(by^>)CVWN-{AO!T?zr zB%wyP3a#iX=kOTQ6%q-|ykU>9kyCM`P?<1z`CUwXP7+3$)7wWR$g}?N(K)4oD5#DR zQHjc$ylJx?n`zU-K1ONFStp}@bf0q~EWM#K!$HB^YnQaItegTUWWl=t_Lj!o{E`w8 z2Y!X=vkiRBp4wg9jnM&o)%=T+d&1F-cxk$lmRGWf*gR2`bOh z&2Gk1d5C_n!K0C~R=~Gri8t|R5P9OKAoFc7aUS1BUnH+Pv}dizJ=OW` zK_tA^$>{pR3!*3^rBb$qr0r*{y4}K|5(h@R8zJ{j(B!Jq{j~jo3}> z$twBZBj`GW4LvMVJf?VJyV2@5WRiJH6U)_A>7*b(0^#6KBy#D57EZv`%z5hT|W1QpXiQ`LFwerV` z@GDN=bNsgt61tq+oCh?-lYe$|%ZSG0FfITu>cM^q!lLhYKe%uETh3WpXUu6Qk}Rg_ zW!~CV;98C}nyrggg)UAzu-Tx?=hPyT43s(HsYq-e{;w9mHcaJs(uOq3Rlzu$*=gy@ z1R%iyHvim1Yh0x%ms?~*UU^XEo?hIA*!+oM0;S8-Z$=w=S{>l2I>Fsh7Tu~y2 z054JbNbl*wl^xqAqoV-t$Y#MNANO`-9Iw~`nHtGpp~9UWW z>CYBf-8*gH3B$4%T(5+(U(vD-Iz=mwc0LZo@N3);k3UlpR*QO*k`J$R?gAw8?^o{S zM@XU@wrTHcmXz7om3|cyK)aw|{3pp{eI|8{=p;=$SMy6%Fso;XRYje-f};0U$^{$* z8)d2`Z_B+@M|Y!)je)IIEE5eCV;%L0I?dtx;0F9ErP6{c=P1n024VLv=#ko2~(G6ynS^GklFjB&$9y zUv+gvt2LpTT9cK-OLIiKhTL+haQjlg|EB^C>qDFGCkqCy3*h{wrCT~8L={cMJK{_Y z;7_1~*`$SvLk$7ti_3H5;f}pPFOD>yzD>R1?L#MUw2@XubOO^VA+ANiC@XjY5S5V$ zV1y+0UEgx9mAjwgh5x<4eES;|@GJ^OhFd@W0lCUhZ<{SaAhQ#+Tw5R(%n zd5Ivgj4)L42?Gc7G#*k%XS^O)!c3WH-A~}FvP&4knX_2N15TSe`KBFr7pO!|;14M| zo$u@2@r(rrq5;Tb`l5rGT4wFg^QcAs_A(}nY}~iUpbHOG52)^17n9eU0ka>gI;;AF zeS@v{)Tqz!KIE9$!Xk>4Rg#?DdPp{AKV|QiVf+gtC{A%%1&0B3h?7=N z+4^FIzk}C=Nj+oG2bc3GAVi8sheqL>|Oe zy6Jp0dm33}8CWgBqHC{=rGXh?)CsyDkgqhuOrK^gmK+D)+)jKIq@S>}N`2G=VqeF! zk-($F<>7yn7l4=l9LY*fXioT7wOz!WYf5UG(j?6lAmPij~w ztH@R2?goEN7xKGSW4J^mGVE1FDCFsD{{hQ7gOOX+=_#wQnp!x-mzhF8#OGb2uia!{0vY6rp-L|4@QS7{sn95vWY==#A zoKmC`b0;VDI6XpJLQ4dpwn)vFl5f{Z!(?}EM}qy5th)qKFkPqxYjyZa65boEZM4(t z($t1-ptV=522W|QA7Yq}AyT6T3BDezfdM88E0<9z%Nn*RKsB!?O?8p0LBo9CGERIv z#0}Z7e<4qIkLO5+mMn}7E)ybbPFKsQjd!R;v)RXP0WAOYEv!At#KMZ~(62z0*l);D zx+oo5Fp^;3-v-K9G|y+q9o1*HS>Vvzci#ArgSxYU;jDd$Q`(S=!Z6`ioebmuOZ5_f zQP`-L5O}37gbF}kLS(hLJOCsyRS+gd2L%&zkp!7)u#9u8i>P5jF@-bUfmu2mpuL61&kh=v_TBWGs4$f z{MQi7zguCiqr^GhgCHQKHXt77guN7!qvHv4&kvikLkkY}jPp z(te;(hp^!+n;XxNUZ7Lcce-h*7s{=pCS z0*CcbSDJr%Fwc4DGQ@Z(1^D$Sbt!|Wrr*Tp+Kqb9#2I{2%vU=aJq!zt{oStKZR*X! z8{jast~>BcL({dUl4(y1YL%c7*;FBMU}Mc}sx^IDb6#XL0mr#iX$hCcqk_cgvPz#- zX4Bd<=b91!Q>()!WZDK=E7>!*N!G?Ot4ySR-xWzOH?7cS(OS8dSBMdjY3`^eJD@ZH zusL7ji{Y~V2a_nJ7pM;#9y?Y{S@ZjsuFRy@FAFcd)TE`N7FM*tD9xyKHzmVZu|5$l zDM|ZErK-!E5%p|_(7Y3<Uck^PK@$Gd>HVt?NPc{qc z<^TPfUl!gSFLE(6&Hbt<* znTjE$gcR;Lm=)MD9sIB8AQW?m!R`8i~ni9|KI0; zLvf>z5u>{}&)$3YfKz+Nw+m233d`40{YQS^$JW;cO70&2f&adTD;vAdpXlN@k3E*t z^M@F84uQ3bPufal^dsaIvaQwXS&?PlQ^Ut9Z4i}s)9K`U4(j^&*OC9u*TJQyk@e{x z;O6G`yyMw-eeKu-3P+DTQ~r8T4)|OP_@p3t9bZ`Zb9JRaC^Fyl|kR$l`|M2vUVUY&v-)XBw_Ga6*Cfl~#Y`1yxX4|gKwzYY)ZBFh# z`<`>&uXA}%!C#hD`E?b?7_9>dMklE5Nsn%NMf6-y#Q`RZ# zEb%M4QR7tAS^MKT%uZ*-F`iNn2b$M@(3+Y%k4`;SXZoMLgmc?Y)^lOEhwjAzr#vrp z3W50|MlsAhg3gB=tO@QAZFz)pu!e`a-{Qa;f%Epfg)j9XN0-`9sa(vQ_3_~d z#s}Cz<z2NU|>6+~{ zl|u5)(&HnL;CLsd?`jD!cK2Hjlb;{LTfiwOgVg7Fh>&rTPGy;CIClBxK%J-wz5xg# zm~7a<;+LGnCu=)Z{aMPvFgD|~A~h6bo)NG}(T>t*r!z&R*)*zZaE;N+NK4*RhsW3e z9O(BW|0ea8%BcKn&gcX@cPw3YLR~ALGUPa`Mh`}}ym`x6Idkh#v@Z3Ux!L-*cw-P# zWTcq}%M?T*N}aoi=96xv)G5-{Z*9;B{KO`#Q>MqIPr53jMhY5?RT3X{*T%>?{8 z8TCNvY13y4L;ju1X(eN7MGO}l|L}CSqsA;r(ldK&T?0ib>6@HPzdB77{`*LQAWZ6@ zrTXhcp|T^YoVi#evwn+%@YqqOm-Z0MYFrc)E9d0Ms8f^)HtZ9!iA}osKQ|vZJi|%8 zdEDnjCC7;xE%Ce&u~|S?V?K%x#{_*;$}U0%n=(B}%r##>laCqA@n@6k@n@L~DSf$m z!<%yE$WtYYV8yt{kSD1o}|Zj;_f-^0@uiOlMy8q%e-pDF6?4m^o6isPbM ztlv^_$~iq{EfC{x3Lj%{(}k+q7V5xZ~_keI@ln4Cpp=CVY=p^>(mNZ_9E z4CKBep&b-jEkcv1z9~G}E`#*H8Mn5mY5n|}`FC_?hVk{wF@JBQ0_#vGlM~%TY5)}9 zM86TAftC_ChZ6K<0WNNC2bnTjKzvqry~EDUOVHdv-sk?-=-^5*&R=o=Xo$y-zLs)| zr7~cR7rw!NT^^?*{VLT+<}p7_A_xp)5dAtbm=J-YxHuE=C>y5tW_=66E>!)TD-p-$KH?DfTt+wI;c4kv=AaZBOgoZ8pRXZ~z-mrJL+!S%SUW@l&HHLtFM6UO2Eui#>1V_&*bOhWU&G}(fU6P+O2 z)d%11WgCE}y6bo927_3JG3S{>&J1xi3bCMQW~JziMq*Ojz1BnR?SPwnON@Ptu%c(V z$y*$;vnA+cbZq{It9F|SorwuU8HC2KrQ#Z@6`8F<{lAl*(euP&X>otBbi_hI9cU6} zc#l}MsdCgmsW&DSi|W{5=6te|*Wl!%a>p{mhp`Q{rJMI4_7n zVtMznC#fCzjTJ3r*W)KwW`-6<<&5=%m`p(1yRkz`=;v0HLi6lORA*_8EY87Yi!4TK zFNTv=h7WWA(}1wv^chs%B__Y}MOGFTCOFwCZAm6gy+ChQP{#HxcM6q6gifnZy1b)= z)im>baA%}Aeu_I=$A_6yA5OEB!Js}zY^%rG?vxr>+rVGC+SovEsl26&-1<1mB*e`= z0L(nWE1*i0L7KBrhr;U2SieQb=bkWBrd)0=$CXI#T7;>iom#UN(+)i_mO4#O>R>{l zxz{=Lv}=97YE#(~b?%#Uo4lEB`I2D_C54)NyB;w>-c-}h`YrD-^9g|v-BjPIaZs|e zh}<05U=WHtz*#_mt~!vTCu+ePW0g9_QqO7@o?36I38dIr+_Ca4I^luJbw^g#KAkfv z!|{{>@G!XDtUx{20&`~#if)>$%z`@SNsDaNb9ai2y(sFK<=o`Q#@A*ALpI2k=Km&_ z`S^)dz#Gt^F^Sln$j_7o8rQRaGGlY5$lrH?MhSOi{~S-_JzW5gcG{0;R> z9{Z~Dz5Tx`Tew`}VYyR=O&3S(?4?0us2l-b`rtnx{!HhsMQ#GdEg}yjk4#rCJ7GjT zw|T%8U6!lqPh?IA;uv!*r$dDkF=~s}gjYSf4+Lc-PaaYj2!x=iEuFm~d=({^>Q7H# z?aeXCJ{Pi^M9PDx|C$ZBN_+cw{rW)LeV_AV(B5aS*?(LKPsG`$0Uxie`nF@UvE3Kx zfLb(0)gDW4YR+AAg!7`vcg`EUb=-k7sS^Nqt<%02b;AVhxBCV@E-qw;w451}e%&dB znwOr_-oT@N!uP9}%@050*FQ*ae^xi&9|)heL9ac8uRZ4Vj(cFJs{3M}(O0Yc((iTH z|8*|sy#e(8(slWA(f#rM???Hrh6$5M1InKidGyPh#42pSf5)*J!7FG%qC5`URl8;# z>SfogfwGZPVwl74j`_7xXH(P)cLr|@2BS)LBs|>Q*Bh~ijgV5m@I|mgg>QVzBEZ>< zu(W@_5)3Q#0EV=xJVsks(>f8q$5;P2s5Q~UZnYkaqVXS!Z&SNU^-B(>-=a&%pgDgq zzJhK{b2AA5+W;Suf(C~yKbyT5TXld?D*5FO7g2U6&LnM*g$=LzEZetgeT7oo@<{nhM1{@G7 zep;=@$~2K^twl$_Ms`z7ZRI)?(D?w`LZEzJUE%qP$UH!Hs#gn0-G2?XEm*H}5s0mv zs-1*WMKKgbBY3NE(rkD6tI2nYlKF|ZQf7LbrTBfqgH6m%F35~N$Y!mshl}w{N?!(s zBO99c5><*k9HHJ(QpL`$z${NsC42}-exTXRt^z%|Ds{`da_azrGCoOX)=+XE86m1|mY&Gzw2^D2`NOM%s%L^>=1Xfs9h+9ETXAb8 zFT1?6qAY_RPsr4#FiJjth?t3=Fx7>d`1J$1wa(JgV>uS>$aF3 ztRg0#JpauJk-*|CGLrDa`ij^8o-vt(-x2yPzN{f3=8hn=+QAz-O|c@xkaG1v0{&h) zHuNv!G$A}+FgL`;>LVimfzrS6>TM)|=!+g;WksIt%J_-E-6QrBQsL+)q6gHY3#TDm zaB2sgvl=tBYzz<{uQwnK`&)#Cg+&!;xcwYiKI}q!YY6RO#x~tH>aFW^x9MI^G@yO1 z!mmC$B#qkMOi54+LhH90Ww+D*7@Xi^@8;{|=4-M2b1~ynFJq_EenhD$^U%2)xLl;d z;OzvfW9t*(?j6+qadsAk4TkHuP}bd6ZS6h!aS1%OAW6K|PxTKwKmLRX+$~T0HQBC$ z;{pFY&c2}bP2XFvAIN!{ABpekCmytm3Sym-Z&jSdK|w*m%SHPoIK=y{=53{>^W4_n zc}j{Y;)r7rr+(e8-43}*ijPEctd)W&ehWjM5}BQE@PweeU*&hl=@NPTA`J*~=LcaApK=oV;r{(k8LxN)jDs`o@Xo!0|sy9-PCLqf$sRi7`6$g z?Iv}-*{Okd`AV0ajw3}HHchd#57)kJ%Ar^r+1_qod}^Ag*!TM@Otssw*A}|yrOZ+2 z5j}QkSu;O&q5jk)N|KU^GaIs|?C2_sqAY0!1wvIrJ7y(P6Qf65xDZ3pfrgpBCN*TY zN=oxa{9NT|+?l&&KgE8wZ?RT{DS&{YdKb#V_UYLH!jSsL{D|;`TLx9~?4yBRW%D#Q6$t)B{iZcl5qP0Ew_{vKc=35v7^XW>su)#I1FxMm;9 z45FVb(ECme#BEg+Kjk~o0;$|fSJS`7gyfxU;~2q-1y{T(G^PA*PMoOAWFeI9`0cY0 ztO0j$79fuS)XMz&%3P;B%6;EKn~i3skC8R_P~ikTv*OG^P)6k|vyK`qX5OS=9q%O) z7Jp|XH_1#flu?>7&D6)dEw*xfkz4dMA+wO)(oPjTZR zkE=p6OCG+fcVa||LrbH8$;`~XADMbU@pS0e_*@dC=8_qWo0Qh+?qpo4dB2yXtDD%~ z>t)aYeueww2{G#LB*H3ITk;<|b^rIJ{v{gPe)Ztta9so;Co?k?nP}Ly*6aq6UI>UX zAqo%A--OuDuG;}Liaoy&M8Mj7yY+Xu{d50Lg^c$Q@TZsCj#CyQRWsfz>yi=Tj%cS? z><%wYgorD6oEQqY;oBtPek^u`9V1L|P{_^uB^VaQ5F7UJb4>UM3Nug$;d*&_)UJ8k zx5MjRyWJIwV>N~p7tNHXdcRt=ic>u|_hy(DkM@@}wo-pts<3*Fu&{CNCxUuFOAruPTA`!~Y?eHJu) z7od^&{$LCe2^BgEMUAH9x4YXNAa!Ce2SsKqGsz ztzTxkHHqb4WSjh|e-$`s?tFi;fW3R0^YMx-^3TrPyzYjJGI~y(!k}k-0G=X&KR3Lk zEQ_(_-C91al3+%uG-bF^{aE+dDdN_l(KuE-(^0pm*j%7&JkjLjS(HU#bNW+hCP7;# zc&vzdlIhyhLo=Gx+CgzicZ(M1>_mDb8GPbl?=~JeOQSMR&LoU(PQfHP3p*Q}LDh&X z#~&|-k=ju@QE3-W{=l_X(9e{W69VUS=X6kazeJb9`uoBDPH{|W%B#{Mtm{JTq@}=x zStuY|GN)C8g-f;{C)+}!iDho{H>xw6fM{`c>LNf{m1X>j#-960X%CuA2hOBpR!=|0 z|5E_es1KE`&II16#@~S6xP|7C5yT%$uS~Ai48AH#8-9}%EyR-Q{?;LK-;;TAM&}#X za@A`CFt9SSQr#f1Rnk+8@gira1!(!>oJmolAGu&zcYM4m%=ELkyL(%uTueX7E>^47 zchGjPY>&`K!7j8dm2#=KQ)4QeZj76aykbnczJ*Ui-#ik#CbBq38S~cKh)xvvU{fcB zp`+tMSB=G}MiWF`f5$+=A|HloDt^`Du=w6U<4S-&qAC3aWym-Vds*-MB7I&96RQeY zb}Ku+m@Wy{D^tCK&|m`lpn0C5{N7z9Zut{aKthLd@>_(lNJZjKo3iy;m${GF)Do0yPUSG%D9k{fJP#^W1a=^SZNkIgi?WqDW)IC zCKR1xA@TO2NZ({dh&Xo=0;0UmxWrqxgZ_pe5ryhB{fsvMmMoIJaZ+^Nu47aN+jnRlB8=si(*Nb6i&c40TSiw*tLq?Fb5-d*i)|$ z6^d&@$QHSjm;B|P`DbT%|K3xC+wH;=`LbDc-0>e_M~kp!swX@&L@?L4Ah^c8^kV+b zXHyqBhz{ogNOaEM_LsbP_}gbNtMt#19Z%Em6b*u-r$gl~IgdN#bbU~N! zh-VPr=^@brfW%R_&sm~a!U!Y*#%8wIz1|8_jtCRwDPxbC<B6UjH9;%r+Oae4u@^ycoRi8xVvFBS$h5;NyoNw^gs&a2Q7=Fx>fq0U*7cd)e4DQL&VxB zp2KyWX*!c>nFk9*0juW^D~&%UDmBcc`8iTj#9Vt#-53lL(4inOhMuL*jZB#Nn8q+i zp}~0>m?I6(&~G#?BYc&5?xYHfI5O6W)vI4+&<2IS?j1+vE@FDeFyiRy224LDoGaOF zQ3t#?9@@&~+r)?&ptZp7n{BaM*A(@EHFt4p zF;;jqe?5VRHeQ;tykcG3`KNaQX;u+Gv6zfr*Qo%JxMiM_C`9vZH}FeS`2A3fm+ zN0@$Sfp2*9kNRAQiK1urg9&_IPvUOKvMg9LOkZEkCk^QH5rb zp!&6u`BR=s#{Q=TpfvncAkg64dO-NnXgC5T`m*$_{sYMY7ecXzkPTnIsyjZS}8 zEVCU4;~9=95h8Fs8|>;K7H@&I;2#OX``c>|8O!jKBbUnCycUlsg#sRM=$1d?#~I~q z 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 eTf4PCs{IcTM@J&vE_jOo0000 -![Plan View](../../../assets/plan/plan_view_overview.jpg) +![Plan View](../../../assets/plan/plan_view_overview.png) ## UI Overview {#ui_overview} -The [screenshot above](#plan_screenshot) shows a simple mission plan that starts with a takeoff at the [Planned Home](#planned_home) position (H), -flies through three waypoints, and then lands on the last waypoint (i.e. waypoint 3). +The [screenshot above](#plan_screenshot) shows the Plan View with an empty mission. +The map is centered on the [Planned Home](#planned_home) position (H). The main elements of the UI are: - **Map:** Displays the numbered indicators for the current mission, including the [Planned Home](#planned_home). Click on the indicators to select them (for editing) or drag them around to reposition them. -- **Plan Toolbar:** Status information for the currently selected waypoint relative to the previous waypoint, as well as statistics for the entire mission (e.g. horizontal distance and time for mission). - - `Max telem dist` is the distance between the [Planned Home](#planned_home) and the furthest waypoint. - - When connected to a vehicle it also shows an **Upload** button, can be used to upload the plan to the vehicle. -- **[Plan Tools](#plan_tools):** Used to create and manage missions. -- **[Mission Command List/Overlay](#mission_command_list):** Displays the current list of mission items (select items to [edit](#mission_command_editors)). -- **Terrain Altitude Overlay:** Shows the relative altitude of each mission command. - -It shows you information related to the currently selected waypoint as well as statistics for the entire mission. + Flight path lines and direction arrows show the planned route between waypoints. +- **Plan Toolbar:** Located at the top of the view with buttons for **Open**, **Save**, **Upload**, and **Clear**. + A hamburger menu (⋮) provides additional options such as _Save as KML_ and _Download_ (load plan from vehicle). + The **Save** and **Upload** buttons are highlighted when there are unsaved or un-uploaded changes. +- **[Plan Tools](#plan_tools):** A vertical tool strip on the left side of the map used to add mission items (Takeoff, Waypoint, Pattern, ROI, Return/Land) and toggle the stats panel. +- **[Plan Editor Panel](#plan_editor_panel):** A collapsible tree view on the right side containing the plan file info, mission items, GeoFence, and rally point editors. +- **Layer Switcher:** Buttons in the top-right area for switching between **Mission**, **Geo-Fence**, and **Rally Point** editing layers. +- **Mission Stats / Terrain Panel:** A panel at the bottom of the map that can toggle between a terrain altitude profile chart (height AMSL vs. distance) and mission statistics (selected waypoint info, total distance, max telemetry distance, estimated flight time, and battery info). ## Planning a Mission {#plan_mission} -At very high level, the steps to create a mission are: +At a very high level, the steps to create a mission are: 1. Change to _Plan View_. 2. Add waypoints or commands to the mission and edit as needed. @@ -41,12 +41,8 @@ The following sections explain some of the details in the view. The _Planned Home_ shown in _Plan View_ is used to set the approximate start point when planning a mission (i.e. when a vehicle may not even be connected to QGC). It is used by QGC to estimate mission times and to draw waypoint lines. -![Planned Home Position](../../../assets/plan/mission/mission_settings_planned_home.jpg) - You should move/drag the planned home position to roughly the location where you plan to takeoff. -The altitude for the planned home position is set in the [Mission Settings](#mission_settings) panel. - - +The altitude for the planned home position is determined automatically from terrain data. :::tip The Fly View displays the _actual_ home position set by the vehicle firmware when it arms (this is where the vehicle will return in Return/RTL mode). @@ -54,132 +50,131 @@ The Fly View displays the _actual_ home position set by the vehicle firmware whe ## Plan Tools {#plan_tools} -The plan tools are used for adding individual waypoints, easing mission creation for complicated geometries, uploading/downloading/saving/restoring missions, and for navigating the map. The main tools are described below. - -::: info -**Center map**, **Zoom In**, **Zoom Out** tools help users better view and navigate the _Plan view_ map (they don't affect the mission commands sent to the vehicle). -::: +The plan tools are a vertical tool strip on the left side of the map, used for adding mission items. The tools are only visible when editing the Mission layer. The main tools (top to bottom) are described below. -### Add Waypoints +### Takeoff -Click on the **Add Waypoint** tool to activate it. While active, clicking on the map will add new mission waypoint at the clicked location. -The tool will stay active until you select it again. -Once you have added a waypoint, you can select it and drag it around to change its position. +Inserts a takeoff command into the mission. This tool is available for all vehicle types except rovers. -### File (Sync) {#file} - -The _File tools_ are used to move missions between the ground station and vehicle, and to save/restore them from files. -The tool displays an `!` to indicate that there are mission changes that you have not sent to the vehicle. - -::: info -Before you fly a mission you must upload it to the vehicle. -::: +### Pattern -The _File tools_ provide the following functionality: +The [Pattern](pattern.md) tool simplifies the creation of missions for flying complex geometries, including [surveys](../plan_view/pattern_survey.md) and [structure scans](../plan_view/pattern_structure_scan_v2.md). +If multiple pattern types are available for the vehicle, clicking the button opens a dropdown to select from Survey, Corridor Scan, Structure Scan, and landing patterns. -- Upload (Send to vehicle) -- Download (Load from vehicle) -- Save/Save as to File, including as KML file. -- Load from File -- Remove All (removes all mission waypoints from _Plan view_ and from vehicle) +### Waypoint -### Pattern +Click the **Waypoint** tool to toggle waypoint-on-click mode. While active, clicking on the map will add a new mission waypoint at the clicked location. +The tool stays active until you click it again to deactivate. +Once you have added a waypoint, you can select it and drag it around to reposition it. -The [Pattern](pattern.md) tool simplifies the creation of missions for flying complex geometries, including [surveys](../plan_view/pattern_survey.md) and [structure scans](../plan_view/pattern_structure_scan_v2.md). +### ROI -## Mission Command List {#mission_command_list} +Toggles Region of Interest mode. When active, clicking on the map sets an ROI location. +This tool is only visible if the vehicle firmware supports ROI mode. -Mission commands for the current mission are listed on the right side of the view. -At the top are a set of options to switch between editing the mission, GeoFence and rally points. -Within the list you can select individual mission items to edit their values. +### Return / Land -![Mission Command List](../../../assets/plan/mission/mission_command_list.jpg) +Adds a return or land command to the mission. The label varies by vehicle type: +- **Multicopters:** _Return_ +- **Fixed-wing:** _Land_ (or _Alt Land_ if a land item already exists in the mission) -### Mission Command Editors {#mission_command_editors} +### Stats -Click on a mission command in the list to display its editor (in which you can set/change the command attributes). +Toggles the [Mission Stats / Terrain panel](#stats_terrain) at the bottom of the map. Only visible when that panel is currently hidden. -You can change the **type** of the command by clicking on the command name (for example: _Waypoint_). -This will display the _Select Mission Command_ dialog shown below. -By default this just displays the "Basic Commands", but you can use the **Category** drop down menu to display more (e.g. choose **All commands** to see all the options). +## File Operations {#file} - +File operations are located in the **Plan Toolbar** at the top of the view: -To the right of each command name is a menu that you can click to access to additional options such as _Insert_ and _Delete_. +- **Open** — Load a plan from a file. +- **Save** — Save the current plan to a file. Highlighted when there are unsaved changes. +- **Upload** — Upload the plan to the vehicle. Highlighted when the plan has un-uploaded changes. +- **Clear** — Remove all mission items, geofence, and rally points. If connected to a vehicle, also clears them from the vehicle. +- **Hamburger menu** (⋮) — Additional options: + - _Save as KML_ — Export the plan as a KML file. + - _Download_ — Download the current plan from the vehicle (only available when connected). ::: info -The list of available commands will depend on firmware and vehicle type. -Examples may include: Waypoint, Start image capture, Jump to item (to repeat mission) and other commands. +Before you fly a mission you must upload it to the vehicle. ::: -### Mission Settings {#mission_settings} +## Plan Editor Panel {#plan_editor_panel} -The _Mission Start_ panel is the first item that appears in the [mission command list](#mission_command_list). -It may be used to specify a number default settings that may affect the start or end of the mission. +The Plan Editor Panel is a collapsible tree view on the right side of the view. +The panel can be collapsed or expanded using the toggle button on its left edge. +It is organized into the following collapsible sections: **Plan Info**, **Defaults**, **Mission**, **GeoFence**, **Rally Points**, and **Transform**. -![Mission Command List - showing mission settings](../../../assets/plan/mission_start.png) +### Layer Switcher {#layer_switcher} -![Mission settings](../../../assets/plan/mission/mission_settings.png) +Buttons in the top-right area of the map allow switching between the **Mission**, **Geo-Fence**, and **Rally Point** editing layers. +The active layer button is always visible; the other layer buttons slide in briefly when toggled and auto-hide after a few seconds. -#### Mission Defaults +### Plan Info {#plan_info} -##### Waypoint alt +The Plan Info section contains general plan-level settings: -Set the default altitude for the first mission item added to a plan (subsequent items take an initial altitude from the previous item). -This can also be used to change the altitude of all items in a plan to the same value; you will be prompted if you change the value when there are items in a plan. +- **Plan File** — An editable name for the plan file. +- **Vehicle Info** — Firmware and vehicle type selectors. When connected to a vehicle these are determined automatically; when planning offline you must set them before adding any mission items so that the correct mission commands are available. +- **Expected Home Position** — The altitude (AMSL) for the planned home position. A **Move To Map Center** button repositions the home marker to the center of the map. This is only the _planned_ home position for estimating mission times and drawing waypoint lines — the actual home position is set by the vehicle when it arms. -##### Flight speed +### Defaults {#mission_settings} -Set a flight speed for the mission that is different than the default mission speed. +The Defaults section sets plan-wide values that apply to new mission items: -#### Mission End +- **Altitude Mode** — Select the altitude reference frame for waypoints. +- **Waypoints Altitude** — The default altitude for the first mission item added (subsequent items take their initial altitude from the previous item). Changing this value when items already exist will prompt to update all items to the new altitude. +- **Flight Speed** — Set a flight speed that differs from the default mission speed. +- **Vehicle Speeds** — Cruise speed (fixed-wing) and/or hover speed (multi-rotor/VTOL), used for estimating mission time. -##### Return to Launch after mission end +### Mission Items {#mission_items} -Check this if you want your vehicle to Return/RTL after the final mission item. +The Mission section lists all mission items (waypoints, commands, patterns, etc.) in order. +Each item can be expanded to edit its parameters. -#### Planned Home Position - -The [Planned Home Position](#planned_home) section allows you to simulate the vehicle's home position while planning a mission. -This allows you to view the waypoint trajectory for your vehicle from takeoff to mission completion. - -![MissionSettings Planned Home Position Section](../../../assets/plan/mission/mission_settings_planned_home_position_section.jpg) +- Click an item to select it on the map and expand its editor. +- Click the **command name** dropdown to change the item type. A dialog shows "Basic Commands" by default; use the **Category** dropdown to see all available commands. +- Each item has a **hamburger menu** (⋮) with options such as _Show all values_, _Move to vehicle position_, _Move to previous item position_, and _Edit position_. +- A **delete button** (trash icon) removes the item. +- Items that are incomplete or missing required values show a **?** status indicator. ::: info -This is only the _planned_ home position and you should place it where you plan to start the vehicle from. -It has no actual impact on flying the mission. -The actual home position of a vehicle is set by the vehicle itself when arming. +The list of available commands depends on firmware and vehicle type. +Examples include: Waypoint, Start image capture, Jump to item (to repeat mission), and other commands. ::: -This section allows you to set the **Altitude** and **Set Home to Map Centre** -(you can move it to another position by dragging it on the map). +#### Initial Camera Settings + +This allows you to set camera options before any mission items take place. +This includes camera actions (take photos by time/distance, start/stop video recording) and gimbal control. + +### GeoFence {#geofence} + +The GeoFence section allows you to define geofence boundaries: -#### Camera +- **Polygon Fences** — Add inclusion or exclusion polygon fences. Each polygon can be toggled between inclusion/exclusion, edited, or deleted. +- **Circular Fences** — Add inclusion or exclusion circular fences with a configurable radius. +- **Breach Return Point** — Optionally set a return point (with altitude) that the vehicle will fly to if it breaches the geofence. -The camera section allows you to specify a camera action to take, control the gimbal and set your camera into photo or video mode. +### Rally Points {#rally_points} -![MissionSettings Camera Section](../../../assets/plan/mission/mission_settings_camera_section.jpg) +The Rally Points section displays and manages rally points. When a rally point is selected, its position fields are shown for editing. -The available camera actions are: +### Transform {#transform} -- No change (continue current action) -- Take photos (time) -- Take photos (distance) -- Stop taking photos -- Start recording video -- Stop recording video +The Transform section provides tools to adjust the entire mission after it has been created: -#### Vehicle Info +- **Offset Mission** — Shift the mission by a specified distance in the East, North, and Up directions. Optionally include takeoff and/or landing items in the offset. +- **Reposition Mission** — Move the mission to a new location specified by geographic coordinates (Lat/Lon), UTM, MGRS, or the current vehicle position. +- **Rotate Mission** — Rotate the mission clockwise by a specified number of degrees. Optionally include takeoff and/or landing items in the rotation. -The appropriate mission commands for the vehicle depend on the firmware and vehicle type. +## Mission Stats / Terrain Panel {#stats_terrain} -If you are planning a mission while you are _connected to a vehicle_ the firmware and vehicle type will be determined from the vehicle. -This section allows you to specify the vehicle firmware/type when not connected to a vehicle. +A panel at the bottom of the map provides two toggleable views: -![MissionSettings VehicleInfoSection](../../../assets/plan/mission/mission_settings_vehicle_info_section.jpg) +- **Terrain Profile** — A chart showing height AMSL vs. distance from start across the mission. +- **Mission Statistics** — Displays details for the selected waypoint (altitude difference, azimuth, distance to previous waypoint, gradient, heading) and for the total mission (distance, max telemetry distance, estimated flight time). Battery information (batteries required and change point) is also shown when available. -The additional value that can be specified when planning a mission is the vehicle flight speed. -By specifying this value, total mission or survey times can be approximated even when not connected to a vehicle. +Toggle buttons on the left side of the panel let you switch between the terrain profile and mission statistics views, or collapse the panel entirely. ## Troubleshooting @@ -197,7 +192,7 @@ The loss rate should be in the low single digits (i.e. maximum of 2 or 3): - Higher loss rates often lead to 100% failure. There is a much smaller possibility that issues are caused by bugs in either flight stack or QGC. -To analyse this possibility you can turn on [Console Logging](../settings_view/console_logging.md) for Plan upload/download and review the protocol message traffic. +To analyze this possibility you can turn on [Console Logging](../settings_view/console_logging.md) for Plan upload/download and review the protocol message traffic. ## Further Info From f72abdc81f96c7f09a1e5ceb460a3f9db009ac5d Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Tue, 10 Mar 2026 13:50:21 -0700 Subject: [PATCH 058/119] docs: Fix image references in translated plan_view docs --- docs/ko/qgc-user-guide/plan_view/plan_view.md | 2 +- docs/tr/qgc-user-guide/plan_view/plan_view.md | 2 +- docs/zh/qgc-user-guide/plan_view/plan_view.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ko/qgc-user-guide/plan_view/plan_view.md b/docs/ko/qgc-user-guide/plan_view/plan_view.md index 49a18d0547db..1d6d6c6b8a3e 100644 --- a/docs/ko/qgc-user-guide/plan_view/plan_view.md +++ b/docs/ko/qgc-user-guide/plan_view/plan_view.md @@ -5,7 +5,7 @@ _계획 뷰_에서는 차량에 대한 _자율 임무_를 계획하고 차량에 펌웨어에서 지원하는 경우 [지오 펜스](plan_geofence.md)와 [랠리 포인트](plan_rally_points.md)를 설정할 수 있습니다. -![계획 뷰 ](../../../assets/plan/plan_view_overview.jpg) +![계획 뷰 ](../../../assets/plan/plan_view_overview.png) ## UI 개요 {#ui_overview} diff --git a/docs/tr/qgc-user-guide/plan_view/plan_view.md b/docs/tr/qgc-user-guide/plan_view/plan_view.md index 77ab88f237cd..4d3a43fae8d3 100644 --- a/docs/tr/qgc-user-guide/plan_view/plan_view.md +++ b/docs/tr/qgc-user-guide/plan_view/plan_view.md @@ -5,7 +5,7 @@ _Plan View_, aracınız için _ otonom görevler _ planlamak ve onları araca y Ayrıca eğer yazılım tarafından destekleniyorsa [GeoFence](plan_geofence.md) ve [Rally Points](plan_rally_points.md)'leri ayalarmak için kullanılır. -![Plan Ekranı](../../../assets/plan/plan_view_overview.jpg) +![Plan Ekranı](../../../assets/plan/plan_view_overview.png) ## Kullanıcı Arayüzü'ne Genel Bakış {#ui_overview} diff --git a/docs/zh/qgc-user-guide/plan_view/plan_view.md b/docs/zh/qgc-user-guide/plan_view/plan_view.md index a2f88ea5e35d..c9813fd5c341 100644 --- a/docs/zh/qgc-user-guide/plan_view/plan_view.md +++ b/docs/zh/qgc-user-guide/plan_view/plan_view.md @@ -5,7 +5,7 @@ _计划视图_ 用于为你的载具规 划自动化任务_ 并上传到载具 如果固件支持,它也可以用来配置 [地理围栏](plan_geofence.md) 和 [集结点](plan_rally_points.md)。 -![计划视图](../../../assets/plan/plan_view_overview.jpg) +![计划视图](../../../assets/plan/plan_view_overview.png) ## 界面概述{#ui_overview} From 84a834524c711eb4ec826918cb99bf5d4164f4e3 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Tue, 10 Mar 2026 13:57:46 -0700 Subject: [PATCH 059/119] docs: Address review feedback - fix menu symbol and home alt description --- docs/en/qgc-user-guide/plan_view/plan_view.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/qgc-user-guide/plan_view/plan_view.md b/docs/en/qgc-user-guide/plan_view/plan_view.md index e5f8eebe5609..5c32aae14b74 100644 --- a/docs/en/qgc-user-guide/plan_view/plan_view.md +++ b/docs/en/qgc-user-guide/plan_view/plan_view.md @@ -18,7 +18,7 @@ The main elements of the UI are: Click on the indicators to select them (for editing) or drag them around to reposition them. Flight path lines and direction arrows show the planned route between waypoints. - **Plan Toolbar:** Located at the top of the view with buttons for **Open**, **Save**, **Upload**, and **Clear**. - A hamburger menu (⋮) provides additional options such as _Save as KML_ and _Download_ (load plan from vehicle). + A hamburger menu (☰) provides additional options such as _Save as KML_ and _Download_ (load plan from vehicle). The **Save** and **Upload** buttons are highlighted when there are unsaved or un-uploaded changes. - **[Plan Tools](#plan_tools):** A vertical tool strip on the left side of the map used to add mission items (Takeoff, Waypoint, Pattern, ROI, Return/Land) and toggle the stats panel. - **[Plan Editor Panel](#plan_editor_panel):** A collapsible tree view on the right side containing the plan file info, mission items, GeoFence, and rally point editors. @@ -90,7 +90,7 @@ File operations are located in the **Plan Toolbar** at the top of the view: - **Save** — Save the current plan to a file. Highlighted when there are unsaved changes. - **Upload** — Upload the plan to the vehicle. Highlighted when the plan has un-uploaded changes. - **Clear** — Remove all mission items, geofence, and rally points. If connected to a vehicle, also clears them from the vehicle. -- **Hamburger menu** (⋮) — Additional options: +- **Hamburger menu** (☰) — Additional options: - _Save as KML_ — Export the plan as a KML file. - _Download_ — Download the current plan from the vehicle (only available when connected). @@ -115,7 +115,7 @@ The Plan Info section contains general plan-level settings: - **Plan File** — An editable name for the plan file. - **Vehicle Info** — Firmware and vehicle type selectors. When connected to a vehicle these are determined automatically; when planning offline you must set them before adding any mission items so that the correct mission commands are available. -- **Expected Home Position** — The altitude (AMSL) for the planned home position. A **Move To Map Center** button repositions the home marker to the center of the map. This is only the _planned_ home position for estimating mission times and drawing waypoint lines — the actual home position is set by the vehicle when it arms. +- **Expected Home Position** — The altitude (AMSL) for the planned home position is determined automatically from terrain data. A **Move To Map Center** button repositions the home marker to the center of the map. This is only the _planned_ home position for estimating mission times and drawing waypoint lines — the actual home position is set by the vehicle when it arms. ### Defaults {#mission_settings} @@ -133,7 +133,7 @@ Each item can be expanded to edit its parameters. - Click an item to select it on the map and expand its editor. - Click the **command name** dropdown to change the item type. A dialog shows "Basic Commands" by default; use the **Category** dropdown to see all available commands. -- Each item has a **hamburger menu** (⋮) with options such as _Show all values_, _Move to vehicle position_, _Move to previous item position_, and _Edit position_. +- Each item has a **hamburger menu** (☰) with options such as _Show all values_, _Move to vehicle position_, _Move to previous item position_, and _Edit position_. - A **delete button** (trash icon) removes the item. - Items that are incomplete or missing required values show a **?** status indicator. From 2a11bac9eac25c0e39bab5bb650c30d7e1927444 Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Wed, 11 Mar 2026 01:19:18 -0400 Subject: [PATCH 060/119] Fix random docs upload workflow failure --- .github/workflows/crowdin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 98c3485d6877..5b9db657219c 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -25,7 +25,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: upload: From f59763d391d6ee7acb7f15f5c1e0ac848f475ce4 Mon Sep 17 00:00:00 2001 From: Zhang3 <108582810+cuav-zhang3@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:17:02 +0800 Subject: [PATCH 061/119] boards: Add CUAV X25-MEGA board --- src/Vehicle/VehicleSetup/FirmwareUpgradeController.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Vehicle/VehicleSetup/FirmwareUpgradeController.cc b/src/Vehicle/VehicleSetup/FirmwareUpgradeController.cc index 55d28659cea7..36993d248a96 100644 --- a/src/Vehicle/VehicleSetup/FirmwareUpgradeController.cc +++ b/src/Vehicle/VehicleSetup/FirmwareUpgradeController.cc @@ -89,7 +89,8 @@ static QMap px4_board_name_map { {7000, "cuav_7-nano_default"}, {7001, "cuav_fmu-v6x_default"}, {7002, "cuav_x25-evo_default"}, - {7003, "cuav_x25-super_default"} + {7003, "cuav_x25-super_default"}, + {7004, "cuav_x25-mega_default"} }; uint qHash(const FirmwareUpgradeController::FirmwareIdentifier& firmwareId) From 2998d59e3faf5ed557b9b2da8e0a89d470768bf9 Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Wed, 11 Mar 2026 01:23:51 -0400 Subject: [PATCH 062/119] fix(linux): Preload system GIO alongside GLib to prevent GstGIO segfault When system GLib is preloaded for TTS compatibility, the bundled GStreamer libgstgio.so plugin crashes due to ABI mismatch between system GLib and bundled GIO. Preload system GIO as well so both libraries come from the same build. Closes #14063 --- deploy/linux/AppRun | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deploy/linux/AppRun b/deploy/linux/AppRun index dc46bb7ca2db..c027c3834efb 100755 --- a/deploy/linux/AppRun +++ b/deploy/linux/AppRun @@ -25,9 +25,14 @@ for LIB_PATH in \ done # Preload system GLib 2.76+ for TTS compatibility (libspeechd.so.2 dependency) +# Must also preload system GIO to avoid ABI mismatch with bundled GStreamer GIO plugin if [ -n "$SYSTEM_GLIB" ]; then if nm -D "$SYSTEM_GLIB" 2>/dev/null | grep -qm1 g_string_free_and_steal; then - export LD_PRELOAD="${SYSTEM_GLIB}${LD_PRELOAD:+:$LD_PRELOAD}" + GLIB_PRELOAD="${SYSTEM_GLIB}" + if [ -f "$SYSTEM_GIO" ]; then + GLIB_PRELOAD="${GLIB_PRELOAD}:${SYSTEM_GIO}" + fi + export LD_PRELOAD="${GLIB_PRELOAD}${LD_PRELOAD:+:$LD_PRELOAD}" fi fi From c33bfd3c28c405effac332c97fcf0b13312975f4 Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Wed, 11 Mar 2026 01:53:36 -0400 Subject: [PATCH 063/119] CI: update ccache to 4.13.1 and fix build cache misses - Bump ccache from 4.12.3 to 4.13.1 - Fix archive naming for 4.13+ (-glibc suffix) - Replace hendrikmuhs/ccache-action with actions/cache for full control over cache save/restore and eliminate config conflicts - Refactor install_ccache.py into ccache_helper.py with subcommands: config (with --target for CI arch resolution), install, summary - Add ccache installation for macOS (brew) and Windows+Android (pinned binary with SHA256 verification), fixing missing ccache on those platforms after ccache-action removal - Add SHA256 verification for all Windows ccache binary downloads - Add job summary with hit/miss table and collapsible verbose stats - Restore CCACHE_CONFIGPATH in CI env for consistent config across all ccache invocations (stats, eviction, builds) - Add post-build cache eviction for entries not touched during the job - Simplify shell in cache action: remove python-finding boilerplate, move version validation and arch mapping into Python tool --- .github/actions/cache/action.yml | 169 ++++++----- .github/actions/cmake-build/action.yml | 14 +- .github/scripts/README.md | 2 +- .../{install_ccache.py => ccache_helper.py} | 281 +++++++++++++++--- .github/scripts/tests/test_ccache_helper.py | 244 +++++++++++++++ .github/scripts/tests/test_install_ccache.py | 112 ------- .github/workflows/linux.yml | 15 +- 7 files changed, 583 insertions(+), 254 deletions(-) rename .github/scripts/{install_ccache.py => ccache_helper.py} (60%) create mode 100644 .github/scripts/tests/test_ccache_helper.py delete mode 100644 .github/scripts/tests/test_install_ccache.py diff --git a/.github/actions/cache/action.yml b/.github/actions/cache/action.yml index 2bc911cf30a3..d66137cda141 100644 --- a/.github/actions/cache/action.yml +++ b/.github/actions/cache/action.yml @@ -51,41 +51,10 @@ runs: - name: Get ccache config id: ccache-config shell: bash - env: - INPUT_TARGET: ${{ inputs.target }} - WORKSPACE: ${{ github.workspace }} run: | - # Requires setup-python from common/action.yml - if command -v python3 >/dev/null 2>&1; then - PY_CMD="python3" - elif command -v python >/dev/null 2>&1; then - PY_CMD="python" - else - echo "Error: Python not found in PATH" >&2 - exit 1 - fi - - # Pin ccache version for reproducibility and to avoid API calls - # Update periodically via Dependabot or manually - CCACHE_VERSION="4.12.3" - - # Validate version format (defense in depth) - if [[ ! "$CCACHE_VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then - echo "Error: Invalid ccache version format: $CCACHE_VERSION" >&2 - exit 1 - fi - - if [[ "${INPUT_TARGET}" == "linux_gcc_arm64" ]]; then - arch="aarch64" - else - arch="x86_64" - fi - - "${PY_CMD}" "${WORKSPACE}/.github/scripts/install_ccache.py" \ - --version "${CCACHE_VERSION}" \ - --arch "${arch}" \ - --config "${WORKSPACE}/tools/configs/ccache.conf" \ - --output-only + python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" config \ + --target "${{ inputs.target }}" \ + --conf "${GITHUB_WORKSPACE}/tools/configs/ccache.conf" - name: Cache ccache binary (Linux) if: runner.os == 'Linux' @@ -98,23 +67,19 @@ runs: - name: Install ccache (Linux) if: runner.os == 'Linux' && steps.ccache-binary.outputs.cache-hit != 'true' shell: bash - env: - WORKSPACE: ${{ github.workspace }} - CCACHE_VER: ${{ steps.ccache-config.outputs.version }} - CCACHE_ARCH: ${{ steps.ccache-config.outputs.arch }} run: | - if command -v python3 >/dev/null 2>&1; then - PY_CMD="python3" - elif command -v python >/dev/null 2>&1; then - PY_CMD="python" - else - echo "Error: Python not found in PATH" >&2 - exit 1 + python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" install \ + --version "${{ steps.ccache-config.outputs.version }}" \ + --arch "${{ steps.ccache-config.outputs.arch }}" + + - name: Install ccache (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + if ! command -v ccache >/dev/null 2>&1; then + brew install ccache fi - "${PY_CMD}" "${WORKSPACE}/.github/scripts/install_ccache.py" \ - --version "${CCACHE_VER}" \ - --arch "${CCACHE_ARCH}" \ - --install + ccache --version - name: Setup sccache (Windows x64) if: runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64' @@ -125,40 +90,63 @@ runs: shell: bash run: echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" - - name: Cache ccache binary (Windows ARM64) - if: runner.os == 'Windows' && inputs.host == 'windows_arm64' - id: ccache-binary-win-arm64 + - name: Determine Windows ccache arch + if: runner.os == 'Windows' && (inputs.target == 'android' || inputs.host == 'windows_arm64') + id: win-ccache + shell: bash + run: | + if [[ "${{ inputs.host }}" == "windows_arm64" ]]; then + echo "arch=aarch64" >> "$GITHUB_OUTPUT" + echo "sha256=5907c8f74dcfff920ee57df55a5cb98164630665926b2ba9085e9f5fb701c58f" >> "$GITHUB_OUTPUT" + else + echo "arch=x86_64" >> "$GITHUB_OUTPUT" + echo "sha256=1c78a0b816a3174d4b170b96294e016a21fb4a577dfd8361e7322f77f85c6348" >> "$GITHUB_OUTPUT" + fi + + - name: Cache ccache binary (Windows) + if: runner.os == 'Windows' && (inputs.target == 'android' || inputs.host == 'windows_arm64') + id: ccache-binary-win uses: actions/cache@v5 with: - path: ${{ runner.temp }}/ccache-${{ steps.ccache-config.outputs.version }}-windows-aarch64/ccache.exe - key: ccache-binary-${{ steps.ccache-config.outputs.version }}-windows-aarch64 + path: ${{ runner.temp }}/ccache-${{ steps.ccache-config.outputs.version }}-windows-${{ steps.win-ccache.outputs.arch }}/ccache.exe + key: ccache-binary-${{ steps.ccache-config.outputs.version }}-windows-${{ steps.win-ccache.outputs.arch }} - - name: Install ccache (Windows ARM64) - if: runner.os == 'Windows' && inputs.host == 'windows_arm64' && steps.ccache-binary-win-arm64.outputs.cache-hit != 'true' + - name: Install ccache (Windows) + if: runner.os == 'Windows' && (inputs.target == 'android' || inputs.host == 'windows_arm64') && steps.ccache-binary-win.outputs.cache-hit != 'true' shell: bash env: CCACHE_VERSION: ${{ steps.ccache-config.outputs.version }} + CCACHE_ARCH: ${{ steps.win-ccache.outputs.arch }} + # Update hashes when bumping CCACHE_VERSION + CCACHE_WIN_SHA256: ${{ steps.win-ccache.outputs.sha256 }} run: | set -euo pipefail - ccache_url="https://github.com/ccache/ccache/releases/download/v${CCACHE_VERSION}/ccache-${CCACHE_VERSION}-windows-aarch64.zip" + ccache_url="https://github.com/ccache/ccache/releases/download/v${CCACHE_VERSION}/ccache-${CCACHE_VERSION}-windows-${CCACHE_ARCH}.zip" temp_dir="$(mktemp -d)" temp_root="${RUNNER_TEMP//\\//}" - install_dir="${temp_root}/ccache-${CCACHE_VERSION}-windows-aarch64" + install_dir="${temp_root}/ccache-${CCACHE_VERSION}-windows-${CCACHE_ARCH}" curl --fail --location --silent --show-error "$ccache_url" -o "${temp_dir}/ccache.zip" + actual=$(python3 -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "${temp_dir}/ccache.zip") + if [[ "$actual" != "${CCACHE_WIN_SHA256}" ]]; then + echo "SHA256 mismatch: ${actual} != ${CCACHE_WIN_SHA256}" >&2 + exit 1 + fi + echo "SHA256 verified" unzip -q "${temp_dir}/ccache.zip" -d "$temp_dir" mkdir -p "$install_dir" - cp "${temp_dir}/ccache-${CCACHE_VERSION}-windows-aarch64/ccache.exe" "${install_dir}/ccache.exe" + cp "${temp_dir}/ccache-${CCACHE_VERSION}-windows-${CCACHE_ARCH}/ccache.exe" "${install_dir}/ccache.exe" - - name: Add ccache to PATH (Windows ARM64) - if: runner.os == 'Windows' && inputs.host == 'windows_arm64' + - name: Add ccache to PATH (Windows) + if: runner.os == 'Windows' && (inputs.target == 'android' || inputs.host == 'windows_arm64') shell: bash env: CCACHE_VERSION: ${{ steps.ccache-config.outputs.version }} + CCACHE_ARCH: ${{ steps.win-ccache.outputs.arch }} run: | set -euo pipefail temp_root="${RUNNER_TEMP//\\//}" - install_dir="${temp_root}/ccache-${CCACHE_VERSION}-windows-aarch64" + install_dir="${temp_root}/ccache-${CCACHE_VERSION}-windows-${CCACHE_ARCH}" if [ ! -f "${install_dir}/ccache.exe" ]; then echo "Error: ccache.exe not found at ${install_dir}/ccache.exe" >&2 exit 1 @@ -167,8 +155,8 @@ runs: echo "$install_dir" >> "$GITHUB_PATH" "${install_dir}/ccache.exe" --version - - name: Configure ccache environment (non-Windows) - if: runner.os != 'Windows' + - name: Configure ccache environment + if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64')" shell: bash run: | workspace_path="${GITHUB_WORKSPACE//\\//}" @@ -195,15 +183,7 @@ runs: shell: bash run: | set -euo pipefail - if command -v python3 >/dev/null 2>&1; then - PY_CMD="python3" - elif command -v python >/dev/null 2>&1; then - PY_CMD="python" - else - echo "Error: Python not found in PATH" >&2 - exit 1 - fi - fingerprint="$("${PY_CMD}" - <<'PY' + fingerprint="$(python3 - <<'PY' import hashlib import re from pathlib import Path @@ -255,21 +235,38 @@ runs: )" echo "fingerprint=${fingerprint}" >> "$GITHUB_OUTPUT" - - name: Setup Build Cache (ccache) - if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64')" - uses: hendrikmuhs/ccache-action@v1.2 + - name: Restore Build Cache (ccache, save) + if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64') && inputs.save-cache == 'true'" + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}-${{ hashFiles('.github/build-config.json', 'tools/configs/ccache.conf') }}-${{ github.run_id }} + restore-keys: | + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}- + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-shared- + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}- + ccache-${{ inputs.host }}-${{ inputs.target }}- + ccache-${{ inputs.host }}- + + - name: Restore Build Cache (ccache, read-only) + if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64') && inputs.save-cache != 'true'" + uses: actions/cache/restore@v5 with: - key: ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}-${{ hashFiles('.github/build-config.json', 'tools/configs/ccache.conf') }} + path: ${{ github.workspace }}/.ccache + key: ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}-${{ hashFiles('.github/build-config.json', 'tools/configs/ccache.conf') }}-${{ github.run_id }} restore-keys: | - ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}- - ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-shared- - ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}- - ${{ inputs.host }}-${{ inputs.target }}- - ${{ inputs.host }}- - max-size: ${{ steps.ccache-config.outputs.max_size }} - verbose: 2 - evict-old-files: job - save: ${{ inputs.save-cache == 'true' }} + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}- + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-shared- + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}- + ccache-${{ inputs.host }}-${{ inputs.target }}- + ccache-${{ inputs.host }}- + + - name: Initialize ccache stats + if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64')" + shell: bash + run: | + ccache -z + echo "CCACHE_START_TIME=$(date +%s)" >> "$GITHUB_ENV" - name: Cache CPM Modules (restore and save) if: inputs.cpm-modules != '' && inputs.save-cache == 'true' diff --git a/.github/actions/cmake-build/action.yml b/.github/actions/cmake-build/action.yml index cdd66d14f3a5..672e5b358c0c 100644 --- a/.github/actions/cmake-build/action.yml +++ b/.github/actions/cmake-build/action.yml @@ -132,9 +132,17 @@ runs: shell: bash run: | if command -v ccache >/dev/null 2>&1; then - echo "::group::ccache statistics" - ccache -s - echo "::endgroup::" + # Evict entries not touched during this job + if [[ -n "${CCACHE_START_TIME:-}" ]]; then + elapsed=$(( $(date +%s) - CCACHE_START_TIME )) + if [[ $elapsed -gt 0 ]]; then + echo "Evicting ccache entries older than ${elapsed}s" + ccache --evict-older-than "${elapsed}s" || true + fi + fi + + # Job summary: hit/miss table + verbose stats (ccache 4.10+) + python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" summary elif command -v sccache >/dev/null 2>&1; then echo "::group::sccache statistics" sccache --show-stats diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 8f212c3dd0e5..f68ec9e3d72a 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -15,7 +15,7 @@ Python helper scripts used by workflows and composite actions in this repository | `find_binary.py` | Locate produced binaries/artifacts in build trees | | `generate_build_results_comment.py` | Generate consolidated PR build-results comment | | `gstreamer_archive.py` | Package GStreamer builds and optionally upload to S3 | -| `install_ccache.py` | Resolve ccache config and install pinned Linux binary | +| `ccache_helper.py` | Ccache CI helper: config output, binary install, build summary | | `size_analysis.py` | Analyze binary size changes | | `workflow_runs.py` | Shared helpers for listing workflow runs via GitHub API | diff --git a/.github/scripts/install_ccache.py b/.github/scripts/ccache_helper.py similarity index 60% rename from .github/scripts/install_ccache.py rename to .github/scripts/ccache_helper.py index 4e96ccd096b5..8ba2558dd9e8 100755 --- a/.github/scripts/install_ccache.py +++ b/.github/scripts/ccache_helper.py @@ -1,18 +1,23 @@ #!/usr/bin/env python3 """ -Install and configure ccache with signature verification. +Ccache helper for CI: install, configure, and report. -Usage: - install_ccache.py [--version VERSION] [--arch ARCH] [--config PATH] [--prefix PATH] +Subcommands: + config Output ccache configuration for GitHub Actions + install Download and install a pinned ccache binary (Linux only) + summary Write a build cache hit/miss summary to GitHub Step Summary -Outputs (for GitHub Actions): - version, arch, max_size +Examples: + ccache_helper.py config --version 4.13.1 --arch x86_64 --conf ccache.conf + ccache_helper.py install --version 4.13.1 --arch x86_64 + ccache_helper.py summary """ from __future__ import annotations import argparse import hashlib +import json import os import platform import re @@ -25,9 +30,13 @@ from pathlib import Path from typing import NamedTuple from urllib.error import HTTPError, URLError -from urllib.request import urlopen, urlretrieve +from urllib.request import urlretrieve +# --------------------------------------------------------------------------- +# Data classes +# --------------------------------------------------------------------------- + class CcacheConfig(NamedTuple): """Configuration values extracted for ccache setup.""" @@ -36,10 +45,14 @@ class CcacheConfig(NamedTuple): max_size: str +# --------------------------------------------------------------------------- +# Installer +# --------------------------------------------------------------------------- + class CcacheInstaller: """Handles ccache installation with signature verification.""" - DEFAULT_VERSION = "4.12.3" + DEFAULT_VERSION = "4.13.1" DEFAULT_MAX_SIZE = "2G" MINISIGN_VERSION = "0.11" MINISIGN_KEY = "RWQX7yXbBedVfI4PNx6FLdFXu9GHUFsr28s4BVGxm4BeybtnX3P06saF" @@ -132,7 +145,7 @@ def download_with_retry(self, url: str, dest: Path) -> bool: def download_with_verify(self, temp_dir: Path) -> Path | None: """Download ccache archive and verify its signature.""" - archive_name = f"ccache-{self.version}-linux-{self.arch}.tar.xz" + archive_name = f"ccache-{self.version}-linux-{self.arch}-glibc.tar.xz" archive_path = temp_dir / archive_name sig_path = temp_dir / f"{archive_name}.minisig" @@ -211,7 +224,7 @@ def verify_signature(self, archive: Path, sig_file: Path, minisign_bin: Path) -> def install(self, archive: Path) -> bool: """Extract and install ccache binary.""" temp_dir = archive.parent - extract_dir = temp_dir / f"ccache-{self.version}-linux-{self.arch}" + extract_dir = temp_dir / f"ccache-{self.version}-linux-{self.arch}-glibc" try: with tarfile.open(archive, "r:xz") as tar: @@ -326,6 +339,120 @@ def _print_version(self) -> None: pass +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +def get_ccache_json_stats() -> dict | None: + """Run ``ccache --print-stats --format=json`` and return parsed dict. + + Returns None when ccache is unavailable or too old (< 4.10). + """ + ccache_bin = shutil.which("ccache") + if not ccache_bin: + return None + + try: + result = subprocess.run( + [ccache_bin, "--print-stats", "--format=json"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return None + return json.loads(result.stdout) + except (OSError, json.JSONDecodeError): + return None + + +def build_summary_markdown(stats: dict) -> str: + """Build a GitHub-flavoured markdown summary table from ccache JSON stats.""" + direct = int(stats.get("direct_cache_hit", 0)) + preprocessed = int(stats.get("preprocessed_cache_hit", 0)) + misses = int(stats.get("cache_miss", 0)) + hits = direct + preprocessed + total = hits + misses + pct = f"{hits / total * 100:.1f}" if total > 0 else "0.0" + + lines = [ + "### CCache Statistics", + "", + "| Metric | Value |", + "|--------|-------|", + f"| Cache hits | {hits} / {total} ({pct}%) |", + f"| Direct hits | {direct} |", + f"| Preprocessed hits | {preprocessed} |", + f"| Misses | {misses} |", + ] + return "\n".join(lines) + "\n" + + +def write_step_summary(markdown: str) -> bool: + """Append *markdown* to ``$GITHUB_STEP_SUMMARY``. + + Returns True on success, False when the env var is unset or write fails. + """ + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + print(markdown) + return True + + try: + with open(summary_path, "a") as f: + f.write(markdown) + return True + except OSError as e: + print(f"Warning: Failed to write step summary: {e}", file=sys.stderr) + return False + + +def get_ccache_verbose_stats() -> str | None: + """Run ``ccache -s -vv`` and return its output.""" + ccache_bin = shutil.which("ccache") + if not ccache_bin: + return None + + try: + result = subprocess.run( + [ccache_bin, "-s", "-vv"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return None + return result.stdout.strip() + except OSError: + return None + + +def run_summary() -> int: + """Collect ccache stats and write a job summary table.""" + parts: list[str] = [] + + stats = get_ccache_json_stats() + if stats is not None: + parts.append(build_summary_markdown(stats)) + else: + print("ccache JSON stats unavailable (ccache missing or < 4.10)", file=sys.stderr) + + verbose = get_ccache_verbose_stats() + if verbose: + parts.append( + "

    \nVerbose stats\n\n" + f"```\n{verbose}\n```\n\n
    \n" + ) + + if parts: + write_step_summary("\n".join(parts)) + return 0 + + +# --------------------------------------------------------------------------- +# GitHub Actions output helper +# --------------------------------------------------------------------------- + def output_github_actions(config: CcacheConfig) -> None: """Write outputs for GitHub Actions.""" github_output = os.environ.get("GITHUB_OUTPUT") @@ -341,77 +468,139 @@ def output_github_actions(config: CcacheConfig) -> None: print(f"Warning: Failed to write GitHub output: {e}", file=sys.stderr) -def parse_args() -> argparse.Namespace: +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +TARGET_ARCH_MAP: dict[str, str] = { + "linux_gcc_arm64": "aarch64", +} + + +def resolve_arch(args: argparse.Namespace) -> str | None: + """Return architecture from --arch, --target, or None for auto-detect.""" + if getattr(args, "arch", None): + return args.arch + target = getattr(args, "target", None) + if target: + return TARGET_ARCH_MAP.get(target) + return None + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: """Parse command line arguments.""" parser = argparse.ArgumentParser( - description="Install and configure ccache with signature verification", + description="Ccache helper for CI: install, configure, and report", formatter_class=argparse.RawDescriptionHelpFormatter, ) - parser.add_argument( + sub = parser.add_subparsers(dest="command") + + # -- config -------------------------------------------------------- + cfg = sub.add_parser("config", help="Output ccache configuration for GitHub Actions") + cfg.add_argument( "--version", default=CcacheInstaller.DEFAULT_VERSION, help=f"ccache version (default: {CcacheInstaller.DEFAULT_VERSION})", ) - parser.add_argument( + cfg_arch = cfg.add_mutually_exclusive_group() + cfg_arch.add_argument( "--arch", choices=["x86_64", "aarch64"], help="Architecture (default: auto-detect)", ) - parser.add_argument( - "--config", + cfg_arch.add_argument( + "--target", + metavar="CI_TARGET", + help="CI build target — resolved to arch (e.g. linux_gcc_arm64 → aarch64)", + ) + cfg.add_argument( + "--conf", type=Path, metavar="PATH", help="Path to ccache.conf for max_size", ) - parser.add_argument( + + # -- install ------------------------------------------------------- + inst = sub.add_parser("install", help="Install a pinned ccache binary (Linux)") + inst.add_argument( + "--version", + default=CcacheInstaller.DEFAULT_VERSION, + help=f"ccache version (default: {CcacheInstaller.DEFAULT_VERSION})", + ) + inst_arch = inst.add_mutually_exclusive_group() + inst_arch.add_argument( + "--arch", + choices=["x86_64", "aarch64"], + help="Architecture (default: auto-detect)", + ) + inst_arch.add_argument( + "--target", + metavar="CI_TARGET", + help="CI build target — resolved to arch (e.g. linux_gcc_arm64 → aarch64)", + ) + inst.add_argument( "--prefix", type=Path, default=None, help="Installation prefix (default: $CCACHE_PREFIX or /usr/local)", ) - parser.add_argument( - "--install", - action="store_true", - help="Install ccache binary (Linux only)", - ) - parser.add_argument( - "--output-only", - action="store_true", - help="Only output config, don't install", - ) - return parser.parse_args() + # -- summary ------------------------------------------------------- + sub.add_parser("summary", help="Write cache hit/miss summary to GitHub Step Summary") + + return parser.parse_args(argv) -def main() -> int: +def main(argv: list[str] | None = None) -> int: """Main entry point.""" - args = parse_args() + args = parse_args(argv) - if not CcacheInstaller.validate_version(args.version): - print(f"Error: Invalid ccache version format: {args.version}", file=sys.stderr) - return 1 + if args.command == "config": + if not CcacheInstaller.validate_version(args.version): + print(f"Error: Invalid ccache version format: {args.version}", file=sys.stderr) + return 1 - installer = CcacheInstaller( - version=args.version, - arch=args.arch, - config_path=args.config, - prefix=args.prefix, - ) + installer = CcacheInstaller( + version=args.version, + arch=resolve_arch(args), + config_path=args.conf, + ) + config = installer.get_config() - config = installer.get_config() + print("ccache configuration:") + print(f" Version: {config.version}") + print(f" Arch: {config.arch}") + print(f" Max Size: {config.max_size}") - print("ccache configuration:") - print(f" Version: {config.version}") - print(f" Arch: {config.arch}") - print(f" Max Size: {config.max_size}") + output_github_actions(config) + return 0 - output_github_actions(config) + if args.command == "install": + if not CcacheInstaller.validate_version(args.version): + print(f"Error: Invalid ccache version format: {args.version}", file=sys.stderr) + return 1 + + installer = CcacheInstaller( + version=args.version, + arch=resolve_arch(args), + prefix=args.prefix, + ) + + config = installer.get_config() + print("ccache configuration:") + print(f" Version: {config.version}") + print(f" Arch: {config.arch}") + print(f" Max Size: {config.max_size}") - if args.install and not args.output_only: if not installer.run_install(): return 1 + return 0 - return 0 + if args.command == "summary": + return run_summary() + + print("Error: a subcommand is required (config, install, summary)", file=sys.stderr) + return 1 if __name__ == "__main__": diff --git a/.github/scripts/tests/test_ccache_helper.py b/.github/scripts/tests/test_ccache_helper.py new file mode 100644 index 000000000000..9a2b8bc9fbfd --- /dev/null +++ b/.github/scripts/tests/test_ccache_helper.py @@ -0,0 +1,244 @@ +"""Tests for ccache_helper.py.""" + +from __future__ import annotations + +import os +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest + +from ccache_helper import ( + CcacheConfig, + CcacheInstaller, + build_summary_markdown, + main, + resolve_arch, + parse_args, + write_step_summary, +) + + +class TestCcacheInstaller: + """Tests for CcacheInstaller class.""" + + def test_validate_version_valid(self): + """Test valid version formats.""" + assert CcacheInstaller.validate_version("4.13.1") + assert CcacheInstaller.validate_version("4.13") + assert CcacheInstaller.validate_version("5.0.0") + + def test_validate_version_invalid(self): + """Test invalid version formats.""" + assert not CcacheInstaller.validate_version("4.12.2.1") + assert not CcacheInstaller.validate_version("v4.12.2") + assert not CcacheInstaller.validate_version("latest") + assert not CcacheInstaller.validate_version("") + + def test_detect_arch_x86_64(self): + """Test x86_64 architecture detection.""" + with patch("platform.machine", return_value="x86_64"): + assert CcacheInstaller.detect_arch() == "x86_64" + + def test_detect_arch_amd64(self): + """Test amd64 (alias) architecture detection.""" + with patch("platform.machine", return_value="amd64"): + assert CcacheInstaller.detect_arch() == "x86_64" + + def test_detect_arch_arm64(self): + """Test arm64 architecture detection.""" + with patch("platform.machine", return_value="arm64"): + assert CcacheInstaller.detect_arch() == "aarch64" + + def test_detect_arch_aarch64(self): + """Test aarch64 architecture detection.""" + with patch("platform.machine", return_value="aarch64"): + assert CcacheInstaller.detect_arch() == "aarch64" + + def test_default_prefix_from_env(self): + """Test prefix from CCACHE_PREFIX environment variable.""" + with patch.dict(os.environ, {"CCACHE_PREFIX": "/custom/path"}): + assert CcacheInstaller._default_prefix() == Path("/custom/path") + + def test_default_prefix_fallback(self): + """Test default prefix fallback to /usr/local.""" + with patch.dict(os.environ, {}, clear=True): + os.environ.pop("CCACHE_PREFIX", None) + assert CcacheInstaller._default_prefix() == Path("/usr/local") + + def test_read_max_size_from_config(self): + """Test reading max_size from ccache.conf.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f: + f.write("max_size = 5G\n") + f.write("compiler_check = content\n") + f.flush() + config_path = Path(f.name) + + try: + installer = CcacheInstaller(config_path=config_path) + assert installer.max_size == "5G" + finally: + config_path.unlink() + + def test_read_max_size_missing_file(self): + """Test max_size default when config file is missing.""" + installer = CcacheInstaller(config_path=Path("/nonexistent/path.conf")) + assert installer.max_size == CcacheInstaller.DEFAULT_MAX_SIZE + + def test_get_config(self): + """Test get_config returns correct named tuple.""" + installer = CcacheInstaller(version="4.10", arch="aarch64") + config = installer.get_config() + + assert isinstance(config, CcacheConfig) + assert config.version == "4.10" + assert config.arch == "aarch64" + assert config.max_size == CcacheInstaller.DEFAULT_MAX_SIZE + + def test_installer_with_custom_prefix(self): + """Test installer with custom prefix.""" + custom_prefix = Path("/opt/ccache") + installer = CcacheInstaller(prefix=custom_prefix) + assert installer.prefix == custom_prefix + + +class TestCcacheConfig: + """Tests for CcacheConfig named tuple.""" + + def test_config_creation(self): + """Test CcacheConfig creation.""" + config = CcacheConfig(version="4.13.1", arch="x86_64", max_size="2G") + assert config.version == "4.13.1" + assert config.arch == "x86_64" + assert config.max_size == "2G" + + def test_config_immutable(self): + """Test CcacheConfig is immutable.""" + config = CcacheConfig(version="4.13.1", arch="x86_64", max_size="2G") + with pytest.raises(AttributeError): + config.version = "5.0.0" + + +class TestBuildSummaryMarkdown: + """Tests for build_summary_markdown function.""" + + def test_typical_stats(self): + stats = { + "direct_cache_hit": 100, + "preprocessed_cache_hit": 20, + "cache_miss": 30, + } + md = build_summary_markdown(stats) + assert "120 / 150 (80.0%)" in md + assert "| Direct hits | 100 |" in md + assert "| Preprocessed hits | 20 |" in md + assert "| Misses | 30 |" in md + + def test_all_hits(self): + stats = {"direct_cache_hit": 50, "preprocessed_cache_hit": 0, "cache_miss": 0} + md = build_summary_markdown(stats) + assert "50 / 50 (100.0%)" in md + + def test_all_misses(self): + stats = {"direct_cache_hit": 0, "preprocessed_cache_hit": 0, "cache_miss": 42} + md = build_summary_markdown(stats) + assert "0 / 42 (0.0%)" in md + + def test_empty_stats(self): + md = build_summary_markdown({}) + assert "0 / 0 (0.0%)" in md + + def test_missing_fields_default_to_zero(self): + stats = {"direct_cache_hit": 5} + md = build_summary_markdown(stats) + assert "5 / 5 (100.0%)" in md + + +class TestWriteStepSummary: + """Tests for write_step_summary function.""" + + def test_writes_to_file(self, tmp_path): + summary_file = tmp_path / "summary.md" + with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(summary_file)}): + assert write_step_summary("hello\n") + assert summary_file.read_text() == "hello\n" + + def test_appends_to_existing(self, tmp_path): + summary_file = tmp_path / "summary.md" + summary_file.write_text("existing\n") + with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(summary_file)}): + write_step_summary("appended\n") + assert summary_file.read_text() == "existing\nappended\n" + + def test_prints_when_no_env(self, capsys): + with patch.dict(os.environ, {}, clear=True): + os.environ.pop("GITHUB_STEP_SUMMARY", None) + write_step_summary("fallback\n") + assert "fallback" in capsys.readouterr().out + + +class TestResolveArch: + """Tests for --target / --arch resolution.""" + + def test_arch_flag(self): + args = parse_args(["config", "--arch", "aarch64"]) + assert resolve_arch(args) == "aarch64" + + def test_target_arm64(self): + args = parse_args(["config", "--target", "linux_gcc_arm64"]) + assert resolve_arch(args) == "aarch64" + + def test_target_unknown_defaults_to_none(self): + args = parse_args(["config", "--target", "linux_gcc_64"]) + assert resolve_arch(args) is None + + def test_neither_returns_none(self): + args = parse_args(["config"]) + assert resolve_arch(args) is None + + def test_arch_and_target_mutually_exclusive(self): + with pytest.raises(SystemExit): + parse_args(["config", "--arch", "x86_64", "--target", "linux_gcc_arm64"]) + + def test_install_target(self): + args = parse_args(["install", "--target", "linux_gcc_arm64"]) + assert resolve_arch(args) == "aarch64" + + +class TestCLI: + """Tests for subcommand dispatch.""" + + def test_no_subcommand_returns_error(self): + assert main([]) == 1 + + def test_config_invalid_version(self): + assert main(["config", "--version", "bad"]) == 1 + + def test_config_valid(self, capsys): + assert main(["config", "--version", "4.13.1"]) == 0 + out = capsys.readouterr().out + assert "4.13.1" in out + + def test_config_with_target(self, capsys): + assert main(["config", "--target", "linux_gcc_arm64"]) == 0 + out = capsys.readouterr().out + assert "aarch64" in out + + def test_summary_without_ccache(self): + with patch("ccache_helper.get_ccache_json_stats", return_value=None), \ + patch("ccache_helper.get_ccache_verbose_stats", return_value=None): + assert main(["summary"]) == 0 + + def test_summary_with_stats(self, tmp_path): + stats = {"direct_cache_hit": 10, "preprocessed_cache_hit": 2, "cache_miss": 5} + verbose = "cache hit (direct): 10\ncache miss: 5" + summary_file = tmp_path / "summary.md" + with patch("ccache_helper.get_ccache_json_stats", return_value=stats), \ + patch("ccache_helper.get_ccache_verbose_stats", return_value=verbose), \ + patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(summary_file)}): + assert main(["summary"]) == 0 + content = summary_file.read_text() + assert "12 / 17" in content + assert "
    " in content + assert "cache hit (direct): 10" in content diff --git a/.github/scripts/tests/test_install_ccache.py b/.github/scripts/tests/test_install_ccache.py deleted file mode 100644 index 337000b143b9..000000000000 --- a/.github/scripts/tests/test_install_ccache.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Tests for install_ccache.py.""" - -from __future__ import annotations - -import os -import tempfile -from pathlib import Path -from unittest.mock import patch - -import pytest - -from install_ccache import CcacheConfig, CcacheInstaller - - -class TestCcacheInstaller: - """Tests for CcacheInstaller class.""" - - def test_validate_version_valid(self): - """Test valid version formats.""" - assert CcacheInstaller.validate_version("4.12.2") - assert CcacheInstaller.validate_version("4.12") - assert CcacheInstaller.validate_version("5.0.0") - - def test_validate_version_invalid(self): - """Test invalid version formats.""" - assert not CcacheInstaller.validate_version("4.12.2.1") - assert not CcacheInstaller.validate_version("v4.12.2") - assert not CcacheInstaller.validate_version("latest") - assert not CcacheInstaller.validate_version("") - - def test_detect_arch_x86_64(self): - """Test x86_64 architecture detection.""" - with patch("platform.machine", return_value="x86_64"): - assert CcacheInstaller.detect_arch() == "x86_64" - - def test_detect_arch_amd64(self): - """Test amd64 (alias) architecture detection.""" - with patch("platform.machine", return_value="amd64"): - assert CcacheInstaller.detect_arch() == "x86_64" - - def test_detect_arch_arm64(self): - """Test arm64 architecture detection.""" - with patch("platform.machine", return_value="arm64"): - assert CcacheInstaller.detect_arch() == "aarch64" - - def test_detect_arch_aarch64(self): - """Test aarch64 architecture detection.""" - with patch("platform.machine", return_value="aarch64"): - assert CcacheInstaller.detect_arch() == "aarch64" - - def test_default_prefix_from_env(self): - """Test prefix from CCACHE_PREFIX environment variable.""" - with patch.dict(os.environ, {"CCACHE_PREFIX": "/custom/path"}): - assert CcacheInstaller._default_prefix() == Path("/custom/path") - - def test_default_prefix_fallback(self): - """Test default prefix fallback to /usr/local.""" - with patch.dict(os.environ, {}, clear=True): - os.environ.pop("CCACHE_PREFIX", None) - assert CcacheInstaller._default_prefix() == Path("/usr/local") - - def test_read_max_size_from_config(self): - """Test reading max_size from ccache.conf.""" - with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f: - f.write("max_size = 5G\n") - f.write("compiler_check = content\n") - f.flush() - config_path = Path(f.name) - - try: - installer = CcacheInstaller(config_path=config_path) - assert installer.max_size == "5G" - finally: - config_path.unlink() - - def test_read_max_size_missing_file(self): - """Test max_size default when config file is missing.""" - installer = CcacheInstaller(config_path=Path("/nonexistent/path.conf")) - assert installer.max_size == CcacheInstaller.DEFAULT_MAX_SIZE - - def test_get_config(self): - """Test get_config returns correct named tuple.""" - installer = CcacheInstaller(version="4.10", arch="aarch64") - config = installer.get_config() - - assert isinstance(config, CcacheConfig) - assert config.version == "4.10" - assert config.arch == "aarch64" - assert config.max_size == CcacheInstaller.DEFAULT_MAX_SIZE - - def test_installer_with_custom_prefix(self): - """Test installer with custom prefix.""" - custom_prefix = Path("/opt/ccache") - installer = CcacheInstaller(prefix=custom_prefix) - assert installer.prefix == custom_prefix - - -class TestCcacheConfig: - """Tests for CcacheConfig named tuple.""" - - def test_config_creation(self): - """Test CcacheConfig creation.""" - config = CcacheConfig(version="4.12.2", arch="x86_64", max_size="2G") - assert config.version == "4.12.2" - assert config.arch == "x86_64" - assert config.max_size == "2G" - - def test_config_immutable(self): - """Test CcacheConfig is immutable.""" - config = CcacheConfig(version="4.12.2", arch="x86_64", max_size="2G") - with pytest.raises(AttributeError): - config.version = "5.0.0" diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 350c3afd014f..faf136d4ef86 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -379,13 +379,16 @@ jobs: - name: Ensure coverage tools are available if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' run: | - if ! command -v ccache >/dev/null 2>&1; then - sudo apt-get install -y --no-install-recommends ccache - fi - # Ensure ccache is at /usr/local/bin where the build's ccache-launcher expects it - if [ ! -f /usr/local/bin/ccache ] && command -v ccache >/dev/null 2>&1; then - sudo ln -s "$(command -v ccache)" /usr/local/bin/ccache + ccache_path="$(command -v ccache 2>/dev/null)" || { + echo "Error: ccache not found — expected pinned binary from cache action" >&2 + exit 1 + } + echo "ccache found at: ${ccache_path}" + if [[ "${ccache_path}" != "/usr/local/bin/ccache" ]]; then + echo "Warning: ccache resolved to ${ccache_path}, expected /usr/local/bin/ccache" >&2 fi + ccache_ver="$(ccache --version | head -1)" + echo "ccache version: ${ccache_ver}" if ! command -v gcovr >/dev/null 2>&1; then pipx install gcovr fi From 301a0a723850dd47dc137356ee1c89219b743712 Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Sat, 14 Feb 2026 22:03:45 -0500 Subject: [PATCH 064/119] Refactor AnalyzeView --- .github/test-duration-baseline.json | 2 +- cmake/CustomOptions.cmake | 3 - qgcimages.qrc | 8 +- src/API/QGCCorePlugin.cc | 14 +- .../AnalyzePage.qml | 10 +- src/AnalyzeView/AnalyzeView.qml | 42 ++-- src/AnalyzeView/CMakeLists.txt | 30 +-- src/AnalyzeView/GeoTag/CMakeLists.txt | 1 + src/AnalyzeView/GeoTag/GeoTagController.h | 1 + src/AnalyzeView/GeoTag/GeoTagPage.qml | 101 ++++---- src/AnalyzeView/LogEntry.cc | 64 ----- src/AnalyzeView/MAVLinkConsole/CMakeLists.txt | 12 + .../MAVLinkConsoleController.cc | 0 .../MAVLinkConsoleController.h | 0 .../MAVLinkConsoleIcon.svg | 0 .../MAVLinkConsolePage.qml | 0 .../MAVLinkInspector/CMakeLists.txt | 20 ++ .../MAVLinkInspector}/MAVLinkChart.qml | 0 .../MAVLinkChartController.cc | 0 .../MAVLinkChartController.h | 0 .../MAVLinkInspector.svg | 0 .../MAVLinkInspectorController.cc | 0 .../MAVLinkInspectorController.h | 0 .../MAVLinkInspectorPage.qml | 0 .../{ => MAVLinkInspector}/MAVLinkMessage.cc | 0 .../{ => MAVLinkInspector}/MAVLinkMessage.h | 0 .../MAVLinkMessageButton.qml | 0 .../MAVLinkMessageField.cc | 0 .../MAVLinkMessageField.h | 0 .../{ => MAVLinkInspector}/MAVLinkSystem.cc | 0 .../{ => MAVLinkInspector}/MAVLinkSystem.h | 0 src/AnalyzeView/MAVLinkLogs/CMakeLists.txt | 14 ++ .../MAVLinkLogController.cc} | 168 ++++++------- .../MAVLinkLogController.h} | 20 +- .../MAVLinkLogs/MAVLinkLogEntry.cc | 64 +++++ .../MAVLinkLogEntry.h} | 18 +- .../MAVLinkLogIcon.svg} | 0 .../MAVLinkLogPage.qml} | 50 ++-- .../{ => Vibration}/VibrationPage.qml | 0 .../{ => Vibration}/VibrationPageIcon.png | Bin src/QmlControls/CMakeLists.txt | 5 - src/QmlControls/QGroundControlQmlGlobal.h | 7 - src/UI/MainWindow.qml | 2 +- test/AnalyzeView/CMakeLists.txt | 16 +- .../AnalyzeView/MAVLinkChartControllerTest.cc | 105 ++++++++ test/AnalyzeView/MAVLinkChartControllerTest.h | 18 ++ .../MAVLinkConsoleControllerTest.cc | 226 ++++++++++++++++++ .../MAVLinkConsoleControllerTest.h | 25 ++ .../MAVLinkInspectorControllerTest.cc | 61 +++++ .../MAVLinkInspectorControllerTest.h | 17 ++ ...nloadTest.cc => MAVLinkLogDownloadTest.cc} | 14 +- ...ownloadTest.h => MAVLinkLogDownloadTest.h} | 2 +- test/AnalyzeView/MAVLinkMessageFieldTest.cc | 127 ++++++++++ test/AnalyzeView/MAVLinkMessageFieldTest.h | 17 ++ test/AnalyzeView/MAVLinkMessageTest.cc | 128 ++++++++++ test/AnalyzeView/MAVLinkMessageTest.h | 17 ++ test/AnalyzeView/MAVLinkSystemTest.cc | 168 +++++++++++++ test/AnalyzeView/MAVLinkSystemTest.h | 20 ++ test/CMakeLists.txt | 8 +- 59 files changed, 1289 insertions(+), 336 deletions(-) rename src/{QmlControls => AnalyzeView}/AnalyzePage.qml (91%) delete mode 100644 src/AnalyzeView/LogEntry.cc create mode 100644 src/AnalyzeView/MAVLinkConsole/CMakeLists.txt rename src/AnalyzeView/{ => MAVLinkConsole}/MAVLinkConsoleController.cc (100%) rename src/AnalyzeView/{ => MAVLinkConsole}/MAVLinkConsoleController.h (100%) rename src/AnalyzeView/{ => MAVLinkConsole}/MAVLinkConsoleIcon.svg (100%) rename src/AnalyzeView/{ => MAVLinkConsole}/MAVLinkConsolePage.qml (100%) create mode 100644 src/AnalyzeView/MAVLinkInspector/CMakeLists.txt rename src/{QmlControls => AnalyzeView/MAVLinkInspector}/MAVLinkChart.qml (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkChartController.cc (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkChartController.h (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkInspector.svg (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkInspectorController.cc (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkInspectorController.h (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkInspectorPage.qml (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkMessage.cc (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkMessage.h (100%) rename src/{QmlControls => AnalyzeView/MAVLinkInspector}/MAVLinkMessageButton.qml (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkMessageField.cc (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkMessageField.h (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkSystem.cc (100%) rename src/AnalyzeView/{ => MAVLinkInspector}/MAVLinkSystem.h (100%) create mode 100644 src/AnalyzeView/MAVLinkLogs/CMakeLists.txt rename src/AnalyzeView/{LogDownloadController.cc => MAVLinkLogs/MAVLinkLogController.cc} (69%) rename src/AnalyzeView/{LogDownloadController.h => MAVLinkLogs/MAVLinkLogController.h} (89%) create mode 100644 src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.cc rename src/AnalyzeView/{LogEntry.h => MAVLinkLogs/MAVLinkLogEntry.h} (86%) rename src/AnalyzeView/{LogDownloadIcon.svg => MAVLinkLogs/MAVLinkLogIcon.svg} (100%) rename src/AnalyzeView/{LogDownloadPage.qml => MAVLinkLogs/MAVLinkLogPage.qml} (68%) rename src/AnalyzeView/{ => Vibration}/VibrationPage.qml (100%) rename src/AnalyzeView/{ => Vibration}/VibrationPageIcon.png (100%) create mode 100644 test/AnalyzeView/MAVLinkChartControllerTest.cc create mode 100644 test/AnalyzeView/MAVLinkChartControllerTest.h create mode 100644 test/AnalyzeView/MAVLinkConsoleControllerTest.cc create mode 100644 test/AnalyzeView/MAVLinkConsoleControllerTest.h create mode 100644 test/AnalyzeView/MAVLinkInspectorControllerTest.cc create mode 100644 test/AnalyzeView/MAVLinkInspectorControllerTest.h rename test/AnalyzeView/{LogDownloadTest.cc => MAVLinkLogDownloadTest.cc} (80%) rename test/AnalyzeView/{LogDownloadTest.h => MAVLinkLogDownloadTest.h} (69%) create mode 100644 test/AnalyzeView/MAVLinkMessageFieldTest.cc create mode 100644 test/AnalyzeView/MAVLinkMessageFieldTest.h create mode 100644 test/AnalyzeView/MAVLinkMessageTest.cc create mode 100644 test/AnalyzeView/MAVLinkMessageTest.h create mode 100644 test/AnalyzeView/MAVLinkSystemTest.cc create mode 100644 test/AnalyzeView/MAVLinkSystemTest.h diff --git a/.github/test-duration-baseline.json b/.github/test-duration-baseline.json index e3967044ba54..02f71856d1fa 100644 --- a/.github/test-duration-baseline.json +++ b/.github/test-duration-baseline.json @@ -117,7 +117,7 @@ "LandingComplexItemTest::LandingComplexItemTest": { "seconds": 0.338131 }, - "LogDownloadTest::LogDownloadTest": { + "MAVLinkLogDownloadTest::MAVLinkLogDownloadTest": { "seconds": 1.7378 }, "MAVLinkLogManagerTest::MAVLinkLogManagerTest": { diff --git a/cmake/CustomOptions.cmake b/cmake/CustomOptions.cmake index 84f86ea59216..4207c7b4dfb4 100644 --- a/cmake/CustomOptions.cmake +++ b/cmake/CustomOptions.cmake @@ -62,9 +62,6 @@ set(QGC_VALGRIND_TIMEOUT_MULTIPLIER 20 CACHE STRING "Timeout multiplier for Valg option(QGC_ENABLE_BZIP2 "Enable BZip2 decompression support" OFF) option(QGC_ENABLE_LZ4 "Enable LZ4 decompression support" OFF) -# MAVLink Inspector is disabled by default due to GPL licensing of QtCharts -# option(QGC_DISABLE_MAVLINK_INSPECTOR "Disable MAVLink Inspector" OFF) - # ============================================================================ # Communication Options # ============================================================================ diff --git a/qgcimages.qrc b/qgcimages.qrc index 6e6547bedd9a..1db8eea91c28 100644 --- a/qgcimages.qrc +++ b/qgcimages.qrc @@ -115,7 +115,7 @@ src/AutoPilotPlugins/PX4/Images/LandMode.svg src/AutoPilotPlugins/PX4/Images/LandModeCopter.svg src/AutoPilotPlugins/APM/Images/LightsComponentIcon.png - src/AnalyzeView/LogDownloadIcon.svg + src/AnalyzeView/MAVLinkLogs/MAVLinkLogIcon.svg src/AutoPilotPlugins/PX4/Images/LowBattery.svg src/AutoPilotPlugins/PX4/Images/LowBatteryLight.svg src/FlightMap/Images/MapAddMission.svg @@ -130,8 +130,8 @@ src/FlightMap/Images/MapSyncChanged.svg src/FlightMap/Images/MapType.svg src/FlightMap/Images/MapTypeBlack.svg - src/AnalyzeView/MAVLinkConsoleIcon.svg - src/AnalyzeView/MAVLinkInspector.svg + src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleIcon.svg + src/AnalyzeView/MAVLinkInspector/MAVLinkInspector.svg src/UI/toolbar/Images/Megaphone.svg src/AutoPilotPlugins/Common/Images/MotorComponentIcon.svg src/AutoPilotPlugins/PX4/Images/no-logging-light.svg @@ -212,7 +212,7 @@ src/AutoPilotPlugins/PX4/Images/VehicleTailDownRotate.png src/AutoPilotPlugins/PX4/Images/VehicleUpsideDown.png src/AutoPilotPlugins/PX4/Images/VehicleUpsideDownRotate.png - src/AnalyzeView/VibrationPageIcon.png + src/AnalyzeView/Vibration/VibrationPageIcon.png src/AutoPilotPlugins/Common/Images/wifi.svg src/UI/toolbar/Images/Yield.svg src/FlightMap/Images/ZoomMinus.svg diff --git a/src/API/QGCCorePlugin.cc b/src/API/QGCCorePlugin.cc index 7b0883387895..06b55ab0e35a 100644 --- a/src/API/QGCCorePlugin.cc +++ b/src/API/QGCCorePlugin.cc @@ -63,26 +63,24 @@ const QVariantList &QGCCorePlugin::analyzePages() { static const QVariantList analyzeList = { QVariant::fromValue(new QmlComponentInfo( - tr("Log Download"), - QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/LogDownloadPage.qml")), - QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/LogDownloadIcon.svg")))), + tr("MAVLink Log"), + QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkLogs/MAVLinkLogPage.qml")), + QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/MAVLinkLogIcon.svg")))), QVariant::fromValue(new QmlComponentInfo( tr("GeoTag Images"), QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/GeoTag/GeoTagPage.qml")), QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/GeoTag/GeoTagIcon.svg")))), QVariant::fromValue(new QmlComponentInfo( tr("MAVLink Console"), - QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkConsolePage.qml")), + QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkConsole/MAVLinkConsolePage.qml")), QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/MAVLinkConsoleIcon.svg")))), -#ifndef QGC_DISABLE_MAVLINK_INSPECTOR QVariant::fromValue(new QmlComponentInfo( tr("MAVLink Inspector"), - QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkInspectorPage.qml")), + QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkInspector/MAVLinkInspectorPage.qml")), QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/MAVLinkInspector.svg")))), -#endif QVariant::fromValue(new QmlComponentInfo( tr("Vibration"), - QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/VibrationPage.qml")), + QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/Vibration/VibrationPage.qml")), QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/VibrationPageIcon")))), }; diff --git a/src/QmlControls/AnalyzePage.qml b/src/AnalyzeView/AnalyzePage.qml similarity index 91% rename from src/QmlControls/AnalyzePage.qml rename to src/AnalyzeView/AnalyzePage.qml index 7f1bdf30ec0d..92fd24873019 100644 --- a/src/QmlControls/AnalyzePage.qml +++ b/src/AnalyzeView/AnalyzePage.qml @@ -10,7 +10,6 @@ Item { anchors.margins: ScreenTools.defaultFontPixelWidth property alias pageComponent: pageLoader.sourceComponent - property alias pageName: pageNameLabel.text property alias pageDescription: pageDescriptionLabel.text property alias headerComponent: headerLoader.sourceComponent property real availableWidth: width - pageLoader.x @@ -40,11 +39,6 @@ Item { anchors.right: floatIcon.visible ? floatIcon.left : parent.right spacing: _margins visible: !ScreenTools.isShortScreen && headerLoader.sourceComponent === null - QGCLabel { - id: pageNameLabel - font.pointSize: ScreenTools.largeFontPointSize - visible: !popped - } QGCLabel { id: pageDescriptionLabel anchors.left: parent.left @@ -77,8 +71,8 @@ Item { fillMode: Image.PreserveAspectFit color: qgcPal.text visible: allowPopout && !popped && !ScreenTools.isMobile - MouseArea { - anchors.fill: parent + QGCMouseArea { + fillItem: floatIcon onClicked: popout() } } diff --git a/src/AnalyzeView/AnalyzeView.qml b/src/AnalyzeView/AnalyzeView.qml index 19f6538978dd..159c9e9c36c5 100644 --- a/src/AnalyzeView/AnalyzeView.qml +++ b/src/AnalyzeView/AnalyzeView.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Window import QtQuick.Controls import QGroundControl @@ -23,10 +22,6 @@ Rectangle { anchors.fill: parent } - GeoTagController { - id: geoController - } - QGCFlickable { id: buttonScroll width: buttonColumn.width @@ -44,37 +39,32 @@ Rectangle { width: _maxButtonWidth spacing: _defaultTextHeight / 2 - property real _maxButtonWidth: 0 - - Component.onCompleted: reflowWidths() - - // I don't know why this does not work - Connections { - target: QGroundControl.settingsManager.appSettings.appFontPointSize - function onValueChanged(value) { buttonColumn.reflowWidths() } - } - - function reflowWidths() { - buttonColumn._maxButtonWidth = 0 - for (var i = 0; i < children.length; i++) { - buttonColumn._maxButtonWidth = Math.max(buttonColumn._maxButtonWidth, children[i].width) - } - for (var j = 0; j < children.length; j++) { - children[j].width = buttonColumn._maxButtonWidth + property real _maxButtonWidth: { + var maxW = 0 + for (var i = 0; i < buttonRepeater.count; i++) { + var item = buttonRepeater.itemAt(i) + if (item) maxW = Math.max(maxW, item.implicitWidth) } + return maxW } Repeater { id: buttonRepeater model: QGroundControl.corePlugin ? QGroundControl.corePlugin.analyzePages : [] - Component.onCompleted: itemAt(0).checked = true + Component.onCompleted: { + if (count > 0) { + itemAt(0).checked = true + panelLoader.source = QGroundControl.corePlugin.analyzePages[0].url + panelLoader.title = QGroundControl.corePlugin.analyzePages[0].title + } + } SubMenuButton { - id: subMenu imageResource: modelData.icon autoExclusive: true text: modelData.title + width: buttonColumn._maxButtonWidth onClicked: { panelLoader.source = modelData.url @@ -108,13 +98,13 @@ Rectangle { anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom - source: "LogDownloadPage.qml" + source: "" property string title Connections { target: panelLoader.item - function onPopout() { mainWindow.createrWindowedAnalyzePage(panelLoader.title, panelLoader.source) } + function onPopout() { mainWindow.createWindowedAnalyzePage(panelLoader.title, panelLoader.source) } } } } diff --git a/src/AnalyzeView/CMakeLists.txt b/src/AnalyzeView/CMakeLists.txt index 1f5bed0434bb..42511311a0a0 100644 --- a/src/AnalyzeView/CMakeLists.txt +++ b/src/AnalyzeView/CMakeLists.txt @@ -4,25 +4,12 @@ # ============================================================================ add_subdirectory(GeoTag) +add_subdirectory(MAVLinkConsole) +add_subdirectory(MAVLinkInspector) +add_subdirectory(MAVLinkLogs) target_sources(${CMAKE_PROJECT_NAME} PRIVATE - LogDownloadController.cc - LogDownloadController.h - LogEntry.cc - LogEntry.h - MAVLinkChartController.cc - MAVLinkChartController.h - MAVLinkConsoleController.cc - MAVLinkConsoleController.h - MAVLinkInspectorController.cc - MAVLinkInspectorController.h - MAVLinkMessage.cc - MAVLinkMessage.h - MAVLinkMessageField.cc - MAVLinkMessageField.h - MAVLinkSystem.cc - MAVLinkSystem.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -37,10 +24,13 @@ qt_add_qml_module(AnalyzeViewModule VERSION 1.0 RESOURCE_PREFIX /qml QML_FILES + AnalyzePage.qml AnalyzeView.qml - LogDownloadPage.qml - MAVLinkConsolePage.qml - MAVLinkInspectorPage.qml - VibrationPage.qml + MAVLinkConsole/MAVLinkConsolePage.qml + MAVLinkInspector/MAVLinkChart.qml + MAVLinkInspector/MAVLinkMessageButton.qml + MAVLinkInspector/MAVLinkInspectorPage.qml + MAVLinkLogs/MAVLinkLogPage.qml + Vibration/VibrationPage.qml NO_PLUGIN ) diff --git a/src/AnalyzeView/GeoTag/CMakeLists.txt b/src/AnalyzeView/GeoTag/CMakeLists.txt index 711107827fe1..07c1094a6baa 100644 --- a/src/AnalyzeView/GeoTag/CMakeLists.txt +++ b/src/AnalyzeView/GeoTag/CMakeLists.txt @@ -36,4 +36,5 @@ qt_add_qml_module(GeoTagModule NO_PLUGIN ) +target_link_libraries(GeoTagModule PRIVATE AnalyzeViewModule) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE GeoTagModule) diff --git a/src/AnalyzeView/GeoTag/GeoTagController.h b/src/AnalyzeView/GeoTag/GeoTagController.h index 5820bcc6565e..495f7b7791c2 100644 --- a/src/AnalyzeView/GeoTag/GeoTagController.h +++ b/src/AnalyzeView/GeoTag/GeoTagController.h @@ -49,6 +49,7 @@ class GeoTagController : public QObject { Q_OBJECT QML_ELEMENT + QML_SINGLETON Q_PROPERTY(QString logFile READ logFile WRITE setLogFile NOTIFY logFileChanged) Q_PROPERTY(QString imageDirectory READ imageDirectory WRITE setImageDirectory NOTIFY imageDirectoryChanged) diff --git a/src/AnalyzeView/GeoTag/GeoTagPage.qml b/src/AnalyzeView/GeoTag/GeoTagPage.qml index d6830f14283a..709973af61bc 100644 --- a/src/AnalyzeView/GeoTag/GeoTagPage.qml +++ b/src/AnalyzeView/GeoTag/GeoTagPage.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import QGroundControl +import QGroundControl.AnalyzeView import QGroundControl.Controls AnalyzePage { @@ -27,7 +28,7 @@ AnalyzePage { Layout.preferredHeight: statusColumn.height + _margin * 2 color: qgcPal.windowShade radius: ScreenTools.defaultFontPixelWidth / 2 - visible: geoController.inProgress || geoController.errorMessage || geoController.taggedCount > 0 + visible: GeoTagController.inProgress || GeoTagController.errorMessage || GeoTagController.taggedCount > 0 ColumnLayout { id: statusColumn @@ -40,12 +41,12 @@ AnalyzePage { RowLayout { Layout.fillWidth: true spacing: _margin - visible: geoController.inProgress + visible: GeoTagController.inProgress BusyIndicator { Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 2 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 2 - running: geoController.inProgress + running: GeoTagController.inProgress } ColumnLayout { @@ -61,32 +62,32 @@ AnalyzePage { Layout.fillWidth: true from: 0 to: 100 - value: geoController.progress + value: GeoTagController.progress } } } QGCLabel { Layout.fillWidth: true - text: geoController.errorMessage + text: GeoTagController.errorMessage color: qgcPal.colorRed font.bold: true wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter - visible: geoController.errorMessage && !geoController.inProgress + visible: GeoTagController.errorMessage && !GeoTagController.inProgress } QGCLabel { Layout.fillWidth: true text: { - if (geoController.taggedCount > 0 && !geoController.inProgress) { - let msg = qsTr("Successfully tagged %1 images").arg(geoController.taggedCount) + if (GeoTagController.taggedCount > 0 && !GeoTagController.inProgress) { + let msg = qsTr("Successfully tagged %1 images").arg(GeoTagController.taggedCount) let details = [] - if (geoController.skippedCount > 0) { - details.push(qsTr("%1 skipped").arg(geoController.skippedCount)) + if (GeoTagController.skippedCount > 0) { + details.push(qsTr("%1 skipped").arg(GeoTagController.skippedCount)) } - if (geoController.failedCount > 0) { - details.push(qsTr("%1 failed").arg(geoController.failedCount)) + if (GeoTagController.failedCount > 0) { + details.push(qsTr("%1 failed").arg(GeoTagController.failedCount)) } if (details.length > 0) { msg += " (" + details.join(", ") + ")" @@ -95,10 +96,10 @@ AnalyzePage { } return "" } - color: geoController.failedCount > 0 ? qgcPal.colorOrange : qgcPal.colorGreen + color: GeoTagController.failedCount > 0 ? qgcPal.colorOrange : qgcPal.colorGreen font.bold: true horizontalAlignment: Text.AlignHCenter - visible: geoController.taggedCount > 0 && !geoController.inProgress + visible: GeoTagController.taggedCount > 0 && !GeoTagController.inProgress } } } @@ -126,12 +127,12 @@ AnalyzePage { Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 1.5 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.5 radius: height / 2 - color: geoController.logFile ? qgcPal.colorGreen : qgcPal.button + color: GeoTagController.logFile ? qgcPal.colorGreen : qgcPal.button QGCLabel { anchors.centerIn: parent - text: geoController.logFile ? "\u2713" : "1" - color: geoController.logFile ? "white" : qgcPal.buttonText + text: GeoTagController.logFile ? "\u2713" : "1" + color: GeoTagController.logFile ? "white" : qgcPal.buttonText font.bold: true } } @@ -148,7 +149,7 @@ AnalyzePage { QGCButton { text: qsTr("Browse...") - enabled: !geoController.inProgress + enabled: !GeoTagController.inProgress onClicked: openLogFile.openForLoad() QGCFileDialog { @@ -157,7 +158,7 @@ AnalyzePage { nameFilters: [qsTr("Flight logs (*.ulg *.bin)"), qsTr("ULog (*.ulg)"), qsTr("DataFlash (*.bin)"), qsTr("All Files (*)")] defaultSuffix: "ulg" onAcceptedForLoad: (file) => { - geoController.logFile = file + GeoTagController.logFile = file close() } } @@ -165,9 +166,9 @@ AnalyzePage { QGCLabel { Layout.fillWidth: true - text: geoController.logFile ? geoController.logFile : qsTr("No file selected") + text: GeoTagController.logFile ? GeoTagController.logFile : qsTr("No file selected") elide: Text.ElideMiddle - opacity: geoController.logFile ? 1.0 : 0.5 + opacity: GeoTagController.logFile ? 1.0 : 0.5 } } } @@ -196,12 +197,12 @@ AnalyzePage { Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 1.5 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.5 radius: height / 2 - color: geoController.imageDirectory ? qgcPal.colorGreen : qgcPal.button + color: GeoTagController.imageDirectory ? qgcPal.colorGreen : qgcPal.button QGCLabel { anchors.centerIn: parent - text: geoController.imageDirectory ? "\u2713" : "2" - color: geoController.imageDirectory ? "white" : qgcPal.buttonText + text: GeoTagController.imageDirectory ? "\u2713" : "2" + color: GeoTagController.imageDirectory ? "white" : qgcPal.buttonText font.bold: true } } @@ -218,7 +219,7 @@ AnalyzePage { QGCButton { text: qsTr("Browse...") - enabled: !geoController.inProgress + enabled: !GeoTagController.inProgress onClicked: selectImageDir.openForLoad() QGCFileDialog { @@ -226,7 +227,7 @@ AnalyzePage { title: qsTr("Select Image Folder") selectFolder: true onAcceptedForLoad: (file) => { - geoController.imageDirectory = file + GeoTagController.imageDirectory = file close() } } @@ -234,9 +235,9 @@ AnalyzePage { QGCLabel { Layout.fillWidth: true - text: geoController.imageDirectory ? geoController.imageDirectory : qsTr("No folder selected") + text: GeoTagController.imageDirectory ? GeoTagController.imageDirectory : qsTr("No folder selected") elide: Text.ElideMiddle - opacity: geoController.imageDirectory ? 1.0 : 0.5 + opacity: GeoTagController.imageDirectory ? 1.0 : 0.5 } } } @@ -287,7 +288,7 @@ AnalyzePage { QGCButton { text: qsTr("Browse...") - enabled: !geoController.inProgress + enabled: !GeoTagController.inProgress onClicked: selectDestDir.openForLoad() QGCFileDialog { @@ -295,7 +296,7 @@ AnalyzePage { title: qsTr("Select Output Folder") selectFolder: true onAcceptedForLoad: (file) => { - geoController.saveDirectory = file + GeoTagController.saveDirectory = file close() } } @@ -304,15 +305,15 @@ AnalyzePage { QGCLabel { Layout.fillWidth: true text: { - if (geoController.saveDirectory) { - return geoController.saveDirectory - } else if (geoController.imageDirectory) { - return geoController.imageDirectory + "/TAGGED" + if (GeoTagController.saveDirectory) { + return GeoTagController.saveDirectory + } else if (GeoTagController.imageDirectory) { + return GeoTagController.imageDirectory + "/TAGGED" } return qsTr("Default: /TAGGED subfolder") } elide: Text.ElideMiddle - opacity: geoController.saveDirectory ? 1.0 : 0.5 + opacity: GeoTagController.saveDirectory ? 1.0 : 0.5 } } } @@ -349,11 +350,11 @@ AnalyzePage { QGCTextField { id: timeOffsetField Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 10 - text: geoController.timeOffsetSecs.toFixed(1) - enabled: !geoController.inProgress + text: GeoTagController.timeOffsetSecs.toFixed(1) + enabled: !GeoTagController.inProgress inputMethodHints: Qt.ImhFormattedNumbersOnly validator: DoubleValidator { bottom: -3600; top: 3600; decimals: 1 } - onEditingFinished: geoController.timeOffsetSecs = parseFloat(text) || 0 + onEditingFinished: GeoTagController.timeOffsetSecs = parseFloat(text) || 0 } QGCLabel { @@ -371,9 +372,9 @@ AnalyzePage { QGCCheckBox { id: previewCheckbox text: qsTr("Preview mode (don't write files)") - checked: geoController.previewMode - enabled: !geoController.inProgress - onClicked: geoController.previewMode = checked + checked: GeoTagController.previewMode + enabled: !GeoTagController.inProgress + onClicked: GeoTagController.previewMode = checked } QGCLabel { @@ -391,20 +392,20 @@ AnalyzePage { Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 20 text: { - if (geoController.inProgress) { + if (GeoTagController.inProgress) { return qsTr("Cancel") - } else if (geoController.previewMode) { + } else if (GeoTagController.previewMode) { return qsTr("Preview") } else { return qsTr("Start Tagging") } } - enabled: (geoController.logFile && geoController.imageDirectory) || geoController.inProgress + enabled: (GeoTagController.logFile && GeoTagController.imageDirectory) || GeoTagController.inProgress onClicked: { - if (geoController.inProgress) { - geoController.cancelTagging() + if (GeoTagController.inProgress) { + GeoTagController.cancelTagging() } else { - geoController.startTagging() + GeoTagController.startTagging() } } } @@ -416,7 +417,7 @@ AnalyzePage { Layout.minimumHeight: ScreenTools.defaultFontPixelHeight * 10 color: qgcPal.windowShade radius: ScreenTools.defaultFontPixelWidth / 2 - visible: geoController.imageModel.count > 0 + visible: GeoTagController.imageModel.count > 0 ColumnLayout { anchors.fill: parent @@ -428,7 +429,7 @@ AnalyzePage { spacing: _margin QGCLabel { - text: qsTr("Images (%1)").arg(geoController.imageModel.count) + text: qsTr("Images (%1)").arg(GeoTagController.imageModel.count) font.bold: true } @@ -477,7 +478,7 @@ AnalyzePage { id: imageListView Layout.fillWidth: true Layout.fillHeight: true - model: geoController.imageModel + model: GeoTagController.imageModel delegate: Rectangle { width: imageListView.width diff --git a/src/AnalyzeView/LogEntry.cc b/src/AnalyzeView/LogEntry.cc deleted file mode 100644 index a50239cb54cc..000000000000 --- a/src/AnalyzeView/LogEntry.cc +++ /dev/null @@ -1,64 +0,0 @@ -#include "LogEntry.h" -#include "QGCApplication.h" -#include "QGCLoggingCategory.h" - -#include - -QGC_LOGGING_CATEGORY(LogEntryLog, "AnalyzeView.QGCLogEntry") - -LogDownloadData::LogDownloadData(QGCLogEntry * const logEntry) - : ID(logEntry->id()) - , entry(logEntry) -{ - // qCDebug(LogEntryLog) << Q_FUNC_INFO << this; -} - -LogDownloadData::~LogDownloadData() -{ - // qCDebug(LogEntryLog) << Q_FUNC_INFO << this; -} - -void LogDownloadData::advanceChunk() -{ - ++current_chunk; - chunk_table = QBitArray(chunkBins(), false); -} - -uint32_t LogDownloadData::chunkBins() const -{ - const qreal num = static_cast((entry->size() - (current_chunk * kChunkSize))) / static_cast(MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); - return qMin(static_cast(qCeil(num)), kTableBins); -} - -uint32_t LogDownloadData::numChunks() const -{ - const qreal num = static_cast(entry->size()) / static_cast(kChunkSize); - return qCeil(num); -} - -bool LogDownloadData::chunkEquals(const bool val) const -{ - return (chunk_table == QBitArray(chunk_table.size(), val)); -} - -/*===========================================================================*/ - -QGCLogEntry::QGCLogEntry(uint logId, const QDateTime &dateTime, uint logSize, bool received, QObject *parent) - : QObject(parent) - , _logID(logId) - , _logSize(logSize) - , _logTimeUTC(dateTime) - , _received(received) -{ - // qCDebug(LogEntryLog) << Q_FUNC_INFO << this; -} - -QGCLogEntry::~QGCLogEntry() -{ - // qCDebug(LogEntryLog) << Q_FUNC_INFO << this; -} - -QString QGCLogEntry::sizeStr() const -{ - return qgcApp()->bigSizeToString(_logSize); -} diff --git a/src/AnalyzeView/MAVLinkConsole/CMakeLists.txt b/src/AnalyzeView/MAVLinkConsole/CMakeLists.txt new file mode 100644 index 000000000000..6cd4a2a3fe55 --- /dev/null +++ b/src/AnalyzeView/MAVLinkConsole/CMakeLists.txt @@ -0,0 +1,12 @@ +# ============================================================================ +# MAVLink Console Module +# MAVLink serial console controller +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + MAVLinkConsoleController.cc + MAVLinkConsoleController.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/AnalyzeView/MAVLinkConsoleController.cc b/src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleController.cc similarity index 100% rename from src/AnalyzeView/MAVLinkConsoleController.cc rename to src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleController.cc diff --git a/src/AnalyzeView/MAVLinkConsoleController.h b/src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleController.h similarity index 100% rename from src/AnalyzeView/MAVLinkConsoleController.h rename to src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleController.h diff --git a/src/AnalyzeView/MAVLinkConsoleIcon.svg b/src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleIcon.svg similarity index 100% rename from src/AnalyzeView/MAVLinkConsoleIcon.svg rename to src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleIcon.svg diff --git a/src/AnalyzeView/MAVLinkConsolePage.qml b/src/AnalyzeView/MAVLinkConsole/MAVLinkConsolePage.qml similarity index 100% rename from src/AnalyzeView/MAVLinkConsolePage.qml rename to src/AnalyzeView/MAVLinkConsole/MAVLinkConsolePage.qml diff --git a/src/AnalyzeView/MAVLinkInspector/CMakeLists.txt b/src/AnalyzeView/MAVLinkInspector/CMakeLists.txt new file mode 100644 index 000000000000..4dfd894f392f --- /dev/null +++ b/src/AnalyzeView/MAVLinkInspector/CMakeLists.txt @@ -0,0 +1,20 @@ +# ============================================================================ +# MAVLink Inspector Module +# MAVLink inspector controller and chart integration +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + MAVLinkChartController.cc + MAVLinkChartController.h + MAVLinkInspectorController.cc + MAVLinkInspectorController.h + MAVLinkMessage.cc + MAVLinkMessage.h + MAVLinkMessageField.cc + MAVLinkMessageField.h + MAVLinkSystem.cc + MAVLinkSystem.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/QmlControls/MAVLinkChart.qml b/src/AnalyzeView/MAVLinkInspector/MAVLinkChart.qml similarity index 100% rename from src/QmlControls/MAVLinkChart.qml rename to src/AnalyzeView/MAVLinkInspector/MAVLinkChart.qml diff --git a/src/AnalyzeView/MAVLinkChartController.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkChartController.cc similarity index 100% rename from src/AnalyzeView/MAVLinkChartController.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkChartController.cc diff --git a/src/AnalyzeView/MAVLinkChartController.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkChartController.h similarity index 100% rename from src/AnalyzeView/MAVLinkChartController.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkChartController.h diff --git a/src/AnalyzeView/MAVLinkInspector.svg b/src/AnalyzeView/MAVLinkInspector/MAVLinkInspector.svg similarity index 100% rename from src/AnalyzeView/MAVLinkInspector.svg rename to src/AnalyzeView/MAVLinkInspector/MAVLinkInspector.svg diff --git a/src/AnalyzeView/MAVLinkInspectorController.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorController.cc similarity index 100% rename from src/AnalyzeView/MAVLinkInspectorController.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorController.cc diff --git a/src/AnalyzeView/MAVLinkInspectorController.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorController.h similarity index 100% rename from src/AnalyzeView/MAVLinkInspectorController.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorController.h diff --git a/src/AnalyzeView/MAVLinkInspectorPage.qml b/src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorPage.qml similarity index 100% rename from src/AnalyzeView/MAVLinkInspectorPage.qml rename to src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorPage.qml diff --git a/src/AnalyzeView/MAVLinkMessage.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessage.cc similarity index 100% rename from src/AnalyzeView/MAVLinkMessage.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessage.cc diff --git a/src/AnalyzeView/MAVLinkMessage.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessage.h similarity index 100% rename from src/AnalyzeView/MAVLinkMessage.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessage.h diff --git a/src/QmlControls/MAVLinkMessageButton.qml b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessageButton.qml similarity index 100% rename from src/QmlControls/MAVLinkMessageButton.qml rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessageButton.qml diff --git a/src/AnalyzeView/MAVLinkMessageField.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessageField.cc similarity index 100% rename from src/AnalyzeView/MAVLinkMessageField.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessageField.cc diff --git a/src/AnalyzeView/MAVLinkMessageField.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessageField.h similarity index 100% rename from src/AnalyzeView/MAVLinkMessageField.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessageField.h diff --git a/src/AnalyzeView/MAVLinkSystem.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkSystem.cc similarity index 100% rename from src/AnalyzeView/MAVLinkSystem.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkSystem.cc diff --git a/src/AnalyzeView/MAVLinkSystem.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkSystem.h similarity index 100% rename from src/AnalyzeView/MAVLinkSystem.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkSystem.h diff --git a/src/AnalyzeView/MAVLinkLogs/CMakeLists.txt b/src/AnalyzeView/MAVLinkLogs/CMakeLists.txt new file mode 100644 index 000000000000..6c378db82e03 --- /dev/null +++ b/src/AnalyzeView/MAVLinkLogs/CMakeLists.txt @@ -0,0 +1,14 @@ +# ============================================================================ +# MAVLink Logs Module +# MAVLink log list and download functionality +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + MAVLinkLogController.cc + MAVLinkLogController.h + MAVLinkLogEntry.cc + MAVLinkLogEntry.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/AnalyzeView/LogDownloadController.cc b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.cc similarity index 69% rename from src/AnalyzeView/LogDownloadController.cc rename to src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.cc index 38b569e85b3d..b3ffd7f083e8 100644 --- a/src/AnalyzeView/LogDownloadController.cc +++ b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.cc @@ -1,6 +1,6 @@ -#include "LogDownloadController.h" +#include "MAVLinkLogController.h" #include "AppSettings.h" -#include "LogEntry.h" +#include "MAVLinkLogEntry.h" #include "MAVLinkProtocol.h" #include "MultiVehicleManager.h" #include "ParameterManager.h" @@ -13,35 +13,35 @@ #include #include -QGC_LOGGING_CATEGORY(LogDownloadControllerLog, "AnalyzeView.LogDownloadController") +QGC_LOGGING_CATEGORY(MAVLinkLogControllerLog, "AnalyzeView.MAVLinkLogController") -LogDownloadController::LogDownloadController(QObject *parent) +MAVLinkLogController::MAVLinkLogController(QObject *parent) : QObject(parent) , _timer(new QTimer(this)) , _logEntriesModel(new QmlObjectListModel(this)) { - qCDebug(LogDownloadControllerLog) << this; + qCDebug(MAVLinkLogControllerLog) << this; - (void) connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &LogDownloadController::_setActiveVehicle); - (void) connect(_timer, &QTimer::timeout, this, &LogDownloadController::_processDownload); + (void) connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &MAVLinkLogController::_setActiveVehicle); + (void) connect(_timer, &QTimer::timeout, this, &MAVLinkLogController::_processDownload); _timer->setSingleShot(false); _setActiveVehicle(MultiVehicleManager::instance()->activeVehicle()); } -LogDownloadController::~LogDownloadController() +MAVLinkLogController::~MAVLinkLogController() { - qCDebug(LogDownloadControllerLog) << this; + qCDebug(MAVLinkLogControllerLog) << this; } -void LogDownloadController::download(const QString &path) +void MAVLinkLogController::download(const QString &path) { const QString dir = path.isEmpty() ? SettingsManager::instance()->appSettings()->logSavePath() : path; _downloadToDirectory(dir); } -void LogDownloadController::_downloadToDirectory(const QString &dir) +void MAVLinkLogController::_downloadToDirectory(const QString &dir) { _receivedAllEntries(); @@ -56,7 +56,7 @@ void LogDownloadController::_downloadToDirectory(const QString &dir) _downloadPath += QDir::separator(); } - QGCLogEntry *const log = _getNextSelected(); + QGCMAVLinkLogEntry *const log = _getNextSelected(); if (log) { log->setStatus(tr("Waiting")); } @@ -65,7 +65,7 @@ void LogDownloadController::_downloadToDirectory(const QString &dir) _receivedAllData(); } -void LogDownloadController::_processDownload() +void MAVLinkLogController::_processDownload() { if (_requestingLogEntries) { _findMissingEntries(); @@ -74,13 +74,13 @@ void LogDownloadController::_processDownload() } } -void LogDownloadController::_findMissingEntries() +void MAVLinkLogController::_findMissingEntries() { const int num_logs = _logEntriesModel->count(); int start = -1; int end = -1; for (int i = 0; i < num_logs; i++) { - const QGCLogEntry *const entry = _logEntriesModel->value(i); + const QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -103,14 +103,14 @@ void LogDownloadController::_findMissingEntries() if (_retries++ > 2) { for (int i = 0; i < num_logs; i++) { - QGCLogEntry *const entry = _logEntriesModel->value(i); + QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); if (entry && !entry->received()) { entry->setStatus(tr("Error")); } } _receivedAllEntries(); - qCWarning(LogDownloadControllerLog) << "Too many errors retreiving log list. Giving up."; + qCWarning(MAVLinkLogControllerLog) << "Too many errors retreiving log list. Giving up."; return; } @@ -124,7 +124,7 @@ void LogDownloadController::_findMissingEntries() _requestLogList(static_cast(start), static_cast(end)); } -void LogDownloadController::_setActiveVehicle(Vehicle *vehicle) +void MAVLinkLogController::_setActiveVehicle(Vehicle *vehicle) { if (vehicle == _vehicle) { return; @@ -132,19 +132,19 @@ void LogDownloadController::_setActiveVehicle(Vehicle *vehicle) if (_vehicle) { _logEntriesModel->clearAndDeleteContents(); - (void) disconnect(_vehicle, &Vehicle::logEntry, this, &LogDownloadController::_logEntry); - (void) disconnect(_vehicle, &Vehicle::logData, this, &LogDownloadController::_logData); + (void) disconnect(_vehicle, &Vehicle::logEntry, this, &MAVLinkLogController::_logEntry); + (void) disconnect(_vehicle, &Vehicle::logData, this, &MAVLinkLogController::_logData); } _vehicle = vehicle; if (_vehicle) { - (void) connect(_vehicle, &Vehicle::logEntry, this, &LogDownloadController::_logEntry); - (void) connect(_vehicle, &Vehicle::logData, this, &LogDownloadController::_logData); + (void) connect(_vehicle, &Vehicle::logEntry, this, &MAVLinkLogController::_logEntry); + (void) connect(_vehicle, &Vehicle::logData, this, &MAVLinkLogController::_logData); } } -void LogDownloadController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t id, uint16_t num_logs, uint16_t last_log_num) +void MAVLinkLogController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t id, uint16_t num_logs, uint16_t last_log_num) { Q_UNUSED(last_log_num); @@ -159,7 +159,7 @@ void LogDownloadController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t } for (int i = 0; i < num_logs; i++) { - QGCLogEntry *const entry = new QGCLogEntry(i); + QGCMAVLinkLogEntry *const entry = new QGCMAVLinkLogEntry(i); _logEntriesModel->append(entry); } } @@ -168,13 +168,13 @@ void LogDownloadController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t if ((size > 0) || (_vehicle->firmwareType() != MAV_AUTOPILOT_ARDUPILOTMEGA)) { id -= _apmOffset; if (id < _logEntriesModel->count()) { - QGCLogEntry *const entry = _logEntriesModel->value(id); + QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(id); entry->setSize(size); entry->setTime(QDateTime::fromSecsSinceEpoch(time_utc)); entry->setReceived(true); entry->setStatus(tr("Available")); } else { - qCWarning(LogDownloadControllerLog) << "Received log entry for out-of-bound index:" << id; + qCWarning(MAVLinkLogControllerLog) << "Received MAVLink log entry for out-of-bound index:" << id; } } } else { @@ -190,17 +190,17 @@ void LogDownloadController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t } } -void LogDownloadController::_receivedAllEntries() +void MAVLinkLogController::_receivedAllEntries() { _timer->stop(); _setListing(false); } -bool LogDownloadController::_entriesComplete() const +bool MAVLinkLogController::_entriesComplete() const { const int num_logs = _logEntriesModel->count(); for (int i = 0; i < num_logs; i++) { - const QGCLogEntry *const entry = _logEntriesModel->value(i); + const QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -213,7 +213,7 @@ bool LogDownloadController::_entriesComplete() const return true; } -void LogDownloadController::_logData(uint32_t ofs, uint16_t id, uint8_t count, const uint8_t *data) +void MAVLinkLogController::_logData(uint32_t ofs, uint16_t id, uint8_t count, const uint8_t *data) { if (!_downloadingLogs || !_downloadData) { return; @@ -221,34 +221,34 @@ void LogDownloadController::_logData(uint32_t ofs, uint16_t id, uint8_t count, c id -= _apmOffset; if (_downloadData->ID != id) { - qCWarning(LogDownloadControllerLog) << "Received log data for wrong log"; + qCWarning(MAVLinkLogControllerLog) << "Received log data for wrong log"; return; } if ((ofs % MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN) != 0) { - qCWarning(LogDownloadControllerLog) << "Ignored misaligned incoming packet @" << ofs; + qCWarning(MAVLinkLogControllerLog) << "Ignored misaligned incoming packet @" << ofs; return; } bool result = false; if (ofs <= _downloadData->entry->size()) { - const uint32_t chunk = ofs / LogDownloadData::kChunkSize; - // qCDebug(LogDownloadControllerLog) << "Received data - Offset:" << ofs << "Chunk:" << chunk; + const uint32_t chunk = ofs / MAVLinkLogDownloadData::kChunkSize; + // qCDebug(MAVLinkLogControllerLog) << "Received data - Offset:" << ofs << "Chunk:" << chunk; if (chunk != _downloadData->current_chunk) { - qCWarning(LogDownloadControllerLog) << "Ignored packet for out of order chunk actual:expected" << chunk << _downloadData->current_chunk; + qCWarning(MAVLinkLogControllerLog) << "Ignored packet for out of order chunk actual:expected" << chunk << _downloadData->current_chunk; return; } - const uint16_t bin = (ofs - (chunk * LogDownloadData::kChunkSize)) / MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; + const uint16_t bin = (ofs - (chunk * MAVLinkLogDownloadData::kChunkSize)) / MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; if (bin >= _downloadData->chunk_table.size()) { - qCWarning(LogDownloadControllerLog) << "Out of range bin received"; + qCWarning(MAVLinkLogControllerLog) << "Out of range bin received"; } else { _downloadData->chunk_table.setBit(bin); } if (_downloadData->file.pos() != ofs) { if (!_downloadData->file.seek(ofs)) { - qCWarning(LogDownloadControllerLog) << "Error while seeking log file offset"; + qCWarning(MAVLinkLogControllerLog) << "Error while seeking log file offset"; return; } } @@ -268,17 +268,17 @@ void LogDownloadController::_logData(uint32_t ofs, uint16_t id, uint8_t count, c } else if (_chunkComplete()) { _downloadData->advanceChunk(); _requestLogData(_downloadData->ID, - _downloadData->current_chunk * LogDownloadData::kChunkSize, + _downloadData->current_chunk * MAVLinkLogDownloadData::kChunkSize, _downloadData->chunk_table.size() * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); } else if ((bin < (_downloadData->chunk_table.size() - 1)) && _downloadData->chunk_table.at(bin + 1)) { // Likely to be grabbing fragments and got to the end of a gap _findMissingData(); } } else { - qCWarning(LogDownloadControllerLog) << "Error while writing log file chunk"; + qCWarning(MAVLinkLogControllerLog) << "Error while writing log file chunk"; } } else { - qCWarning(LogDownloadControllerLog) << "Received log offset greater than expected"; + qCWarning(MAVLinkLogControllerLog) << "Received log offset greater than expected"; } if (!result) { @@ -286,7 +286,7 @@ void LogDownloadController::_logData(uint32_t ofs, uint16_t id, uint8_t count, c } } -void LogDownloadController::_findMissingData() +void MAVLinkLogController::_findMissingData() { if (_logComplete()) { _receivedAllData(); @@ -315,12 +315,12 @@ void LogDownloadController::_findMissingData() } } - const uint32_t pos = (_downloadData->current_chunk * LogDownloadData::kChunkSize) + (start * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); + const uint32_t pos = (_downloadData->current_chunk * MAVLinkLogDownloadData::kChunkSize) + (start * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); const uint32_t len = (end - start) * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; _requestLogData(_downloadData->ID, pos, len, _retries); } -void LogDownloadController::_updateDataRate() +void MAVLinkLogController::_updateDataRate() { constexpr uint kSizeUpdateThreshold = 102400; // 0.1 MB const bool timeThresholdMet = _downloadData->elapsed.elapsed() >= kGUIRateMs; @@ -350,17 +350,17 @@ void LogDownloadController::_updateDataRate() _downloadData->last_status_written = _downloadData->written; } -bool LogDownloadController::_chunkComplete() const +bool MAVLinkLogController::_chunkComplete() const { return _downloadData->chunkEquals(true); } -bool LogDownloadController::_logComplete() const +bool MAVLinkLogController::_logComplete() const { return (_chunkComplete() && ((_downloadData->current_chunk + 1) == _downloadData->numChunks())); } -void LogDownloadController::_receivedAllData() +void MAVLinkLogController::_receivedAllData() { _timer->stop(); if (_prepareLogDownload()) { @@ -372,11 +372,11 @@ void LogDownloadController::_receivedAllData() } } -bool LogDownloadController::_prepareLogDownload() +bool MAVLinkLogController::_prepareLogDownload() { _downloadData.reset(); - QGCLogEntry *const entry = _getNextSelected(); + QGCMAVLinkLogEntry *const entry = _getNextSelected(); if (!entry) { return false; } @@ -386,7 +386,7 @@ bool LogDownloadController::_prepareLogDownload() const QString ftime = (entry->time().date().year() >= 2010) ? entry->time().toString(QStringLiteral("yyyy-M-d-hh-mm-ss")) : QStringLiteral("UnknownDate"); - _downloadData = std::make_unique(entry); + _downloadData = std::make_unique(entry); _downloadData->filename = QStringLiteral("log_") + QString::number(entry->id()) + "_" + ftime; if (_vehicle->firmwareType() == MAV_AUTOPILOT_PX4) { @@ -415,9 +415,9 @@ bool LogDownloadController::_prepareLogDownload() bool result = false; if (!_downloadData->file.open(QIODevice::WriteOnly)) { - qCWarning(LogDownloadControllerLog) << "Failed to create log file:" << _downloadData->filename; + qCWarning(MAVLinkLogControllerLog) << "Failed to create log file:" << _downloadData->filename; } else if (!_downloadData->file.resize(entry->size())) { - qCWarning(LogDownloadControllerLog) << "Failed to allocate space for log file:" << _downloadData->filename; + qCWarning(MAVLinkLogControllerLog) << "Failed to allocate space for log file:" << _downloadData->filename; } else { _downloadData->current_chunk = 0; _downloadData->chunk_table = QBitArray(_downloadData->chunkBins(), false); @@ -437,17 +437,17 @@ bool LogDownloadController::_prepareLogDownload() return result; } -void LogDownloadController::refresh() +void MAVLinkLogController::refresh() { _logEntriesModel->clearAndDeleteContents(); _requestLogList(0, 0xffff); } -QGCLogEntry *LogDownloadController::_getNextSelected() const +QGCMAVLinkLogEntry *MAVLinkLogController::_getNextSelected() const { const int numLogs = _logEntriesModel->count(); for (int i = 0; i < numLogs; i++) { - QGCLogEntry *const entry = _logEntriesModel->value(i); + QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -460,7 +460,7 @@ QGCLogEntry *LogDownloadController::_getNextSelected() const return nullptr; } -void LogDownloadController::cancel() +void MAVLinkLogController::cancel() { _requestLogEnd(); _receivedAllEntries(); @@ -478,11 +478,11 @@ void LogDownloadController::cancel() _setDownloading(false); } -void LogDownloadController::_resetSelection(bool canceled) +void MAVLinkLogController::_resetSelection(bool canceled) { const int num_logs = _logEntriesModel->count(); for (int i = 0; i < num_logs; i++) { - QGCLogEntry *const entry = _logEntriesModel->value(i); + QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -498,16 +498,16 @@ void LogDownloadController::_resetSelection(bool canceled) emit selectionChanged(); } -void LogDownloadController::eraseAll() +void MAVLinkLogController::eraseAll() { if (!_vehicle) { - qCWarning(LogDownloadControllerLog) << "Vehicle Unavailable"; + qCWarning(MAVLinkLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(LogDownloadControllerLog) << "Link Unavailable"; + qCWarning(MAVLinkLogControllerLog) << "Link Unavailable"; return; } @@ -522,23 +522,23 @@ void LogDownloadController::eraseAll() ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(LogDownloadControllerLog) << "Failed to send"; + qCWarning(MAVLinkLogControllerLog) << "Failed to send"; return; } refresh(); } -void LogDownloadController::_requestLogList(uint32_t start, uint32_t end) +void MAVLinkLogController::_requestLogList(uint32_t start, uint32_t end) { if (!_vehicle) { - qCWarning(LogDownloadControllerLog) << "Vehicle Unavailable"; + qCWarning(MAVLinkLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(LogDownloadControllerLog) << "Link Unavailable"; + qCWarning(MAVLinkLogControllerLog) << "Link Unavailable"; return; } @@ -555,30 +555,30 @@ void LogDownloadController::_requestLogList(uint32_t start, uint32_t end) ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(LogDownloadControllerLog) << "Failed to send"; + qCWarning(MAVLinkLogControllerLog) << "Failed to send"; return; } - qCDebug(LogDownloadControllerLog) << "Request log entry list (" << start << "through" << end << ")"; + qCDebug(MAVLinkLogControllerLog) << "Request MAVLink log entry list (" << start << "through" << end << ")"; _setListing(true); _timer->start(kRequestLogListTimeoutMs); } -void LogDownloadController::_requestLogData(uint16_t id, uint32_t offset, uint32_t count, int retryCount) +void MAVLinkLogController::_requestLogData(uint16_t id, uint32_t offset, uint32_t count, int retryCount) { if (!_vehicle) { - qCWarning(LogDownloadControllerLog) << "Vehicle Unavailable"; + qCWarning(MAVLinkLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(LogDownloadControllerLog) << "Link Unavailable"; + qCWarning(MAVLinkLogControllerLog) << "Link Unavailable"; return; } id += _apmOffset; - qCDebug(LogDownloadControllerLog) << "Request log data (id:" << id << "offset:" << offset << "size:" << count << "retryCount" << retryCount << ")"; + qCDebug(MAVLinkLogControllerLog) << "Request log data (id:" << id << "offset:" << offset << "size:" << count << "retryCount" << retryCount << ")"; mavlink_message_t msg{}; (void) mavlink_msg_log_request_data_pack_chan( @@ -594,20 +594,20 @@ void LogDownloadController::_requestLogData(uint16_t id, uint32_t offset, uint32 ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(LogDownloadControllerLog) << "Failed to send"; + qCWarning(MAVLinkLogControllerLog) << "Failed to send"; } } -void LogDownloadController::_requestLogEnd() +void MAVLinkLogController::_requestLogEnd() { if (!_vehicle) { - qCWarning(LogDownloadControllerLog) << "Vehicle Unavailable"; + qCWarning(MAVLinkLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(LogDownloadControllerLog) << "Link Unavailable"; + qCWarning(MAVLinkLogControllerLog) << "Link Unavailable"; return; } @@ -622,11 +622,11 @@ void LogDownloadController::_requestLogEnd() ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(LogDownloadControllerLog) << "Failed to send"; + qCWarning(MAVLinkLogControllerLog) << "Failed to send"; } } -void LogDownloadController::_setDownloading(bool active) +void MAVLinkLogController::_setDownloading(bool active) { if (_downloadingLogs != active) { _downloadingLogs = active; @@ -635,7 +635,7 @@ void LogDownloadController::_setDownloading(bool active) } } -void LogDownloadController::_setListing(bool active) +void MAVLinkLogController::_setListing(bool active) { if (_requestingLogEntries != active) { _requestingLogEntries = active; @@ -644,7 +644,7 @@ void LogDownloadController::_setListing(bool active) } } -void LogDownloadController::setCompressLogs(bool compress) +void MAVLinkLogController::setCompressLogs(bool compress) { if (_compressLogs != compress) { _compressLogs = compress; @@ -652,25 +652,25 @@ void LogDownloadController::setCompressLogs(bool compress) } } -bool LogDownloadController::compressLogFile(const QString &logPath) +bool MAVLinkLogController::compressLogFile(const QString &logPath) { Q_UNUSED(logPath) - qCWarning(LogDownloadControllerLog) << "Log compression not yet implemented (decompression-only API)"; + qCWarning(MAVLinkLogControllerLog) << "Log compression not yet implemented (decompression-only API)"; return false; } -void LogDownloadController::cancelCompression() +void MAVLinkLogController::cancelCompression() { // Not implemented - compression API is decompression-only } -void LogDownloadController::_handleCompressionProgress(qreal progress) +void MAVLinkLogController::_handleCompressionProgress(qreal progress) { Q_UNUSED(progress) // Not implemented - compression API is decompression-only } -void LogDownloadController::_handleCompressionFinished(bool success) +void MAVLinkLogController::_handleCompressionFinished(bool success) { Q_UNUSED(success) // Not implemented - compression API is decompression-only diff --git a/src/AnalyzeView/LogDownloadController.h b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.h similarity index 89% rename from src/AnalyzeView/LogDownloadController.h rename to src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.h index b999f14601d6..4bfc18022324 100644 --- a/src/AnalyzeView/LogDownloadController.h +++ b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.h @@ -4,17 +4,17 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(LogDownloadControllerLog) +Q_DECLARE_LOGGING_CATEGORY(MAVLinkLogControllerLog) -struct LogDownloadData; -class QGCLogEntry; +struct MAVLinkLogDownloadData; +class QGCMAVLinkLogEntry; class QmlObjectListModel; class QTimer; class QThread; class Vehicle; -class LogDownloadTest; +class MAVLinkLogDownloadTest; -class LogDownloadController : public QObject +class MAVLinkLogController : public QObject { Q_OBJECT QML_ELEMENT @@ -28,11 +28,11 @@ class LogDownloadController : public QObject Q_PROPERTY(bool compressing READ compressing NOTIFY compressingChanged) Q_PROPERTY(float compressionProgress READ compressionProgress NOTIFY compressionProgressChanged) - friend class LogDownloadTest; + friend class MAVLinkLogDownloadTest; public: - explicit LogDownloadController(QObject *parent = nullptr); - ~LogDownloadController(); + explicit MAVLinkLogController(QObject *parent = nullptr); + ~MAVLinkLogController(); Q_INVOKABLE void refresh(); Q_INVOKABLE void download(const QString &path = QString()); @@ -89,7 +89,7 @@ private slots: void _setListing(bool active); void _updateDataRate(); - QGCLogEntry *_getNextSelected() const; + QGCMAVLinkLogEntry *_getNextSelected() const; QTimer *_timer = nullptr; QmlObjectListModel *_logEntriesModel = nullptr; @@ -98,7 +98,7 @@ private slots: bool _requestingLogEntries = false; int _apmOffset = 0; int _retries = 0; - std::unique_ptr _downloadData; + std::unique_ptr _downloadData; QString _downloadPath; Vehicle *_vehicle = nullptr; bool _compressLogs = false; diff --git a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.cc b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.cc new file mode 100644 index 000000000000..b2b64dcfe1ed --- /dev/null +++ b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.cc @@ -0,0 +1,64 @@ +#include "MAVLinkLogEntry.h" +#include "QGCApplication.h" +#include "QGCLoggingCategory.h" + +#include + +QGC_LOGGING_CATEGORY(MAVLinkLogEntryLog, "AnalyzeView.QGCMAVLinkLogEntry") + +MAVLinkLogDownloadData::MAVLinkLogDownloadData(QGCMAVLinkLogEntry * const logEntry) + : ID(logEntry->id()) + , entry(logEntry) +{ + // qCDebug(MAVLinkLogEntryLog) << Q_FUNC_INFO << this; +} + +MAVLinkLogDownloadData::~MAVLinkLogDownloadData() +{ + // qCDebug(MAVLinkLogEntryLog) << Q_FUNC_INFO << this; +} + +void MAVLinkLogDownloadData::advanceChunk() +{ + ++current_chunk; + chunk_table = QBitArray(chunkBins(), false); +} + +uint32_t MAVLinkLogDownloadData::chunkBins() const +{ + const qreal num = static_cast((entry->size() - (current_chunk * kChunkSize))) / static_cast(MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); + return qMin(static_cast(qCeil(num)), kTableBins); +} + +uint32_t MAVLinkLogDownloadData::numChunks() const +{ + const qreal num = static_cast(entry->size()) / static_cast(kChunkSize); + return qCeil(num); +} + +bool MAVLinkLogDownloadData::chunkEquals(const bool val) const +{ + return (chunk_table == QBitArray(chunk_table.size(), val)); +} + +/*===========================================================================*/ + +QGCMAVLinkLogEntry::QGCMAVLinkLogEntry(uint logId, const QDateTime &dateTime, uint logSize, bool received, QObject *parent) + : QObject(parent) + , _logID(logId) + , _logSize(logSize) + , _logTimeUTC(dateTime) + , _received(received) +{ + // qCDebug(MAVLinkLogEntryLog) << Q_FUNC_INFO << this; +} + +QGCMAVLinkLogEntry::~QGCMAVLinkLogEntry() +{ + // qCDebug(MAVLinkLogEntryLog) << Q_FUNC_INFO << this; +} + +QString QGCMAVLinkLogEntry::sizeStr() const +{ + return qgcApp()->bigSizeToString(_logSize); +} diff --git a/src/AnalyzeView/LogEntry.h b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.h similarity index 86% rename from src/AnalyzeView/LogEntry.h rename to src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.h index 829ac87af423..61fa4549e194 100644 --- a/src/AnalyzeView/LogEntry.h +++ b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.h @@ -10,14 +10,14 @@ #include "MAVLinkLib.h" -class QGCLogEntry; +class QGCMAVLinkLogEntry; -Q_DECLARE_LOGGING_CATEGORY(LogEntryLog) +Q_DECLARE_LOGGING_CATEGORY(MAVLinkLogEntryLog) -struct LogDownloadData +struct MAVLinkLogDownloadData { - explicit LogDownloadData(QGCLogEntry * const logEntry); - ~LogDownloadData(); + explicit MAVLinkLogDownloadData(QGCMAVLinkLogEntry * const logEntry); + ~MAVLinkLogDownloadData(); void advanceChunk(); @@ -31,7 +31,7 @@ struct LogDownloadData bool chunkEquals(const bool val) const; uint ID = 0; - QGCLogEntry *const entry = nullptr; + QGCMAVLinkLogEntry *const entry = nullptr; QBitArray chunk_table; uint32_t current_chunk = 0; @@ -49,7 +49,7 @@ struct LogDownloadData /*===========================================================================*/ -class QGCLogEntry : public QObject +class QGCMAVLinkLogEntry : public QObject { Q_OBJECT // QML_ELEMENT @@ -63,8 +63,8 @@ class QGCLogEntry : public QObject Q_PROPERTY(QString status READ status NOTIFY statusChanged) public: - explicit QGCLogEntry(uint logId, const QDateTime &dateTime = QDateTime(), uint logSize = 0, bool received = false, QObject *parent = nullptr); - ~QGCLogEntry(); + explicit QGCMAVLinkLogEntry(uint logId, const QDateTime &dateTime = QDateTime(), uint logSize = 0, bool received = false, QObject *parent = nullptr); + ~QGCMAVLinkLogEntry(); uint id() const { return _logID; } uint size() const { return _logSize; } diff --git a/src/AnalyzeView/LogDownloadIcon.svg b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogIcon.svg similarity index 100% rename from src/AnalyzeView/LogDownloadIcon.svg rename to src/AnalyzeView/MAVLinkLogs/MAVLinkLogIcon.svg diff --git a/src/AnalyzeView/LogDownloadPage.qml b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogPage.qml similarity index 68% rename from src/AnalyzeView/LogDownloadPage.qml rename to src/AnalyzeView/MAVLinkLogs/MAVLinkLogPage.qml index bdd15f03c426..8c3736d3a6c9 100644 --- a/src/AnalyzeView/LogDownloadPage.qml +++ b/src/AnalyzeView/MAVLinkLogs/MAVLinkLogPage.qml @@ -7,9 +7,9 @@ import QGroundControl import QGroundControl.Controls AnalyzePage { - id: logDownloadPage + id: mavlinkLogPage pageComponent: pageComponent - pageDescription: qsTr("Log Download allows you to download binary log files from your vehicle. Click Refresh to get list of available logs.") + pageDescription: qsTr("MAVLink Log allows you to download binary log files from your vehicle. Click Refresh to get list of available logs.") Component { id: pageComponent @@ -18,7 +18,7 @@ AnalyzePage { width: availableWidth height: availableHeight - Component.onCompleted: LogDownloadController.refresh() + Component.onCompleted: MAVLinkLogController.refresh() QGCFlickable { Layout.fillWidth: true @@ -28,7 +28,7 @@ AnalyzePage { GridLayout { id: gridLayout - rows: LogDownloadController.model.count + 1 + rows: MAVLinkLogController.model.count + 1 columns: 5 flow: GridLayout.TopToBottom columnSpacing: ScreenTools.defaultFontPixelWidth @@ -40,7 +40,7 @@ AnalyzePage { } Repeater { - model: LogDownloadController.model + model: MAVLinkLogController.model QGCCheckBox { Binding on checkState { @@ -54,7 +54,7 @@ AnalyzePage { QGCLabel { text: qsTr("Id") } Repeater { - model: LogDownloadController.model + model: MAVLinkLogController.model QGCLabel { text: object.id } } @@ -62,7 +62,7 @@ AnalyzePage { QGCLabel { text: qsTr("Date") } Repeater { - model: LogDownloadController.model + model: MAVLinkLogController.model QGCLabel { text: { @@ -82,7 +82,7 @@ AnalyzePage { QGCLabel { text: qsTr("Size") } Repeater { - model: LogDownloadController.model + model: MAVLinkLogController.model QGCLabel { text: object.sizeStr } } @@ -90,7 +90,7 @@ AnalyzePage { QGCLabel { text: qsTr("Status") } Repeater { - model: LogDownloadController.model + model: MAVLinkLogController.model QGCLabel { text: object.status } } @@ -104,40 +104,40 @@ AnalyzePage { QGCButton { Layout.fillWidth: true - enabled: !LogDownloadController.requestingList && !LogDownloadController.downloadingLogs + enabled: !MAVLinkLogController.requestingList && !MAVLinkLogController.downloadingLogs text: qsTr("Refresh") onClicked: { if (!QGroundControl.multiVehicleManager.activeVehicle || QGroundControl.multiVehicleManager.activeVehicle.isOfflineEditingVehicle) { - QGroundControl.showMessageDialog(logDownloadPage, qsTr("Log Refresh"), qsTr("You must be connected to a vehicle in order to download logs.")) + QGroundControl.showMessageDialog(mavlinkLogPage, qsTr("MAVLink Log Refresh"), qsTr("You must be connected to a vehicle in order to download MAVLink logs.")) return } - LogDownloadController.refresh() + MAVLinkLogController.refresh() } } QGCButton { Layout.fillWidth: true - enabled: !LogDownloadController.requestingList && !LogDownloadController.downloadingLogs + enabled: !MAVLinkLogController.requestingList && !MAVLinkLogController.downloadingLogs text: qsTr("Download") onClicked: { var logsSelected = false - for (var i = 0; i < LogDownloadController.model.count; i++) { - if (LogDownloadController.model.get(i).selected) { + for (var i = 0; i < MAVLinkLogController.model.count; i++) { + if (MAVLinkLogController.model.get(i).selected) { logsSelected = true break } } if (!logsSelected) { - QGroundControl.showMessageDialog(logDownloadPage, qsTr("Log Download"), qsTr("You must select at least one log file to download.")) + QGroundControl.showMessageDialog(mavlinkLogPage, qsTr("MAVLink Log"), qsTr("You must select at least one MAVLink log file to download.")) return } if (ScreenTools.isMobile) { - LogDownloadController.download() + MAVLinkLogController.download() return } @@ -150,7 +150,7 @@ AnalyzePage { QGCFileDialog { id: fileDialog onAcceptedForLoad: (file) => { - LogDownloadController.download(file) + MAVLinkLogController.download(file) close() } } @@ -158,22 +158,22 @@ AnalyzePage { QGCButton { Layout.fillWidth: true - enabled: !LogDownloadController.requestingList && !LogDownloadController.downloadingLogs && (LogDownloadController.model.count > 0) + enabled: !MAVLinkLogController.requestingList && !MAVLinkLogController.downloadingLogs && (MAVLinkLogController.model.count > 0) text: qsTr("Erase All") onClicked: QGroundControl.showMessageDialog( - logDownloadPage, - qsTr("Delete All Log Files"), - qsTr("All log files will be erased permanently. Is this really what you want?"), + mavlinkLogPage, + qsTr("Delete All MAVLink Log Files"), + qsTr("All MAVLink log files will be erased permanently. Is this really what you want?"), Dialog.Yes | Dialog.No, - function() { LogDownloadController.eraseAll() } + function() { MAVLinkLogController.eraseAll() } ) } QGCButton { Layout.fillWidth: true text: qsTr("Cancel") - enabled: LogDownloadController.requestingList || LogDownloadController.downloadingLogs - onClicked: LogDownloadController.cancel() + enabled: MAVLinkLogController.requestingList || MAVLinkLogController.downloadingLogs + onClicked: MAVLinkLogController.cancel() } } } diff --git a/src/AnalyzeView/VibrationPage.qml b/src/AnalyzeView/Vibration/VibrationPage.qml similarity index 100% rename from src/AnalyzeView/VibrationPage.qml rename to src/AnalyzeView/Vibration/VibrationPage.qml diff --git a/src/AnalyzeView/VibrationPageIcon.png b/src/AnalyzeView/Vibration/VibrationPageIcon.png similarity index 100% rename from src/AnalyzeView/VibrationPageIcon.png rename to src/AnalyzeView/Vibration/VibrationPageIcon.png diff --git a/src/QmlControls/CMakeLists.txt b/src/QmlControls/CMakeLists.txt index 2265877a8962..13dc893c8eac 100644 --- a/src/QmlControls/CMakeLists.txt +++ b/src/QmlControls/CMakeLists.txt @@ -114,8 +114,6 @@ qt_add_qml_module(QGroundControlControlsModule LabelledLabel.qml LogReplayStatusBar.qml MainWindowSavedState.qml - MAVLinkChart.qml - MAVLinkMessageButton.qml MissionCommandDialog.qml MvPanelPage.qml OfflineMapButton.qml @@ -187,9 +185,6 @@ qt_add_qml_module(QGroundControlControlsModule # qt_add_qml_module(). On Windows this cross-directory property propagation fails, # so these files are kept in this module until a better solution is available. - # src/AnalyzeView/ - AnalyzePage.qml - # src/FirstRunPromptDialogs/ FirstRunPrompt.qml diff --git a/src/QmlControls/QGroundControlQmlGlobal.h b/src/QmlControls/QGroundControlQmlGlobal.h index 5464cc04467a..68d83dfcf6c9 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.h +++ b/src/QmlControls/QGroundControlQmlGlobal.h @@ -97,7 +97,6 @@ class QGroundControlQmlGlobal : public QObject Q_PROPERTY(qreal zOrderTrajectoryLines READ zOrderTrajectoryLines CONSTANT) Q_PROPERTY(qreal zOrderWaypointLines READ zOrderWaypointLines CONSTANT) Q_PROPERTY(bool hasAPMSupport READ hasAPMSupport CONSTANT) - Q_PROPERTY(bool hasMAVLinkInspector READ hasMAVLinkInspector CONSTANT) //------------------------------------------------------------------------- @@ -191,12 +190,6 @@ class QGroundControlQmlGlobal : public QObject bool hasAPMSupport () { return true; } #endif -#if defined(QGC_DISABLE_MAVLINK_INSPECTOR) - bool hasMAVLinkInspector () { return false; } -#else - bool hasMAVLinkInspector () { return true; } -#endif - QString elevationProviderName (); QString elevationProviderNotice (); diff --git a/src/UI/MainWindow.qml b/src/UI/MainWindow.qml index 7c10f4c84262..80e0d28592da 100644 --- a/src/UI/MainWindow.qml +++ b/src/UI/MainWindow.qml @@ -608,7 +608,7 @@ ApplicationWindow { // to mainWindow. Otherwise if they are rooted to the AnalyzeView itself they will die when the analyze viewSwitch // closes. - function createrWindowedAnalyzePage(title, source) { + function createWindowedAnalyzePage(title, source) { var windowedPage = windowedAnalyzePage.createObject(mainWindow) windowedPage.title = title windowedPage.source = source diff --git a/test/AnalyzeView/CMakeLists.txt b/test/AnalyzeView/CMakeLists.txt index 072d813ffe5d..9602acf24b36 100644 --- a/test/AnalyzeView/CMakeLists.txt +++ b/test/AnalyzeView/CMakeLists.txt @@ -7,8 +7,20 @@ add_subdirectory(GeoTag) target_sources(${CMAKE_PROJECT_NAME} PRIVATE - LogDownloadTest.cc - LogDownloadTest.h + MAVLinkChartControllerTest.cc + MAVLinkChartControllerTest.h + MAVLinkConsoleControllerTest.cc + MAVLinkConsoleControllerTest.h + MAVLinkInspectorControllerTest.cc + MAVLinkInspectorControllerTest.h + MAVLinkLogDownloadTest.cc + MAVLinkLogDownloadTest.h + MAVLinkMessageFieldTest.cc + MAVLinkMessageFieldTest.h + MAVLinkMessageTest.cc + MAVLinkMessageTest.h + MAVLinkSystemTest.cc + MAVLinkSystemTest.h MavlinkLogTest.cc MavlinkLogTest.h ) diff --git a/test/AnalyzeView/MAVLinkChartControllerTest.cc b/test/AnalyzeView/MAVLinkChartControllerTest.cc new file mode 100644 index 000000000000..85952b729853 --- /dev/null +++ b/test/AnalyzeView/MAVLinkChartControllerTest.cc @@ -0,0 +1,105 @@ +#include "MAVLinkChartControllerTest.h" + +#include + +#include "MAVLinkChartController.h" +#include "MAVLinkInspectorController.h" + +void MAVLinkChartControllerTest::_constructionTest() +{ + MAVLinkChartController chart; + + QVERIFY(chart.inspectorController() == nullptr); + QCOMPARE(chart.chartIndex(), 0); +} + +void MAVLinkChartControllerTest::_defaultRangeValuesTest() +{ + MAVLinkChartController chart; + + QCOMPARE(chart.rangeXIndex(), static_cast(0)); + QCOMPARE(chart.rangeYIndex(), static_cast(0)); + QCOMPARE_FUZZY(chart.rangeYMin(), 0.0, 1e-9); + QCOMPARE_FUZZY(chart.rangeYMax(), 1.0, 1e-9); +} + +void MAVLinkChartControllerTest::_chartFieldsInitiallyEmptyTest() +{ + MAVLinkChartController chart; + + QVERIFY(chart.chartFields().isEmpty()); +} + +void MAVLinkChartControllerTest::_setRangeXIndexTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + chart.setInspectorController(&controller); + + QSignalSpy xIndexSpy(&chart, &MAVLinkChartController::rangeXIndexChanged); + + // Move from default index 0 to index 1 + chart.setRangeXIndex(1); + QCOMPARE(chart.rangeXIndex(), static_cast(1)); + QCOMPARE(xIndexSpy.count(), 1); +} + +void MAVLinkChartControllerTest::_setRangeXIndexSameValueNoSignalTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + chart.setInspectorController(&controller); + + QSignalSpy xIndexSpy(&chart, &MAVLinkChartController::rangeXIndexChanged); + + // Setting the same index (already 0) must not emit + chart.setRangeXIndex(0); + QCOMPARE(xIndexSpy.count(), 0); +} + +void MAVLinkChartControllerTest::_setRangeYIndexTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + chart.setInspectorController(&controller); + + QSignalSpy yIndexSpy(&chart, &MAVLinkChartController::rangeYIndexChanged); + + // Index 1 corresponds to the first non-Auto range (10,000) + chart.setRangeYIndex(1); + QCOMPARE(chart.rangeYIndex(), static_cast(1)); + QCOMPARE(yIndexSpy.count(), 1); + + // Non-Auto range: rangeYMin and rangeYMax must be set symmetrically + const qreal expectedRange = controller.rangeSt()[1]->range; + QCOMPARE_FUZZY(chart.rangeYMin(), -expectedRange, 1e-9); + QCOMPARE_FUZZY(chart.rangeYMax(), expectedRange, 1e-9); +} + +void MAVLinkChartControllerTest::_setRangeYIndexSameValueNoSignalTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + chart.setInspectorController(&controller); + + QSignalSpy yIndexSpy(&chart, &MAVLinkChartController::rangeYIndexChanged); + + // Default is already 0; setting to 0 again must not emit + chart.setRangeYIndex(0); + QCOMPARE(yIndexSpy.count(), 0); +} + +void MAVLinkChartControllerTest::_setInspectorControllerTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + + chart.setInspectorController(&controller); + QCOMPARE(chart.inspectorController(), &controller); + + // Setting the same controller again must be a no-op (no crash, same pointer) + chart.setInspectorController(&controller); + QCOMPARE(chart.inspectorController(), &controller); +} + +UT_REGISTER_TEST(MAVLinkChartControllerTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkChartControllerTest.h b/test/AnalyzeView/MAVLinkChartControllerTest.h new file mode 100644 index 000000000000..2b6c77372111 --- /dev/null +++ b/test/AnalyzeView/MAVLinkChartControllerTest.h @@ -0,0 +1,18 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkChartControllerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _defaultRangeValuesTest(); + void _chartFieldsInitiallyEmptyTest(); + void _setRangeXIndexTest(); + void _setRangeXIndexSameValueNoSignalTest(); + void _setRangeYIndexTest(); + void _setRangeYIndexSameValueNoSignalTest(); + void _setInspectorControllerTest(); +}; diff --git a/test/AnalyzeView/MAVLinkConsoleControllerTest.cc b/test/AnalyzeView/MAVLinkConsoleControllerTest.cc new file mode 100644 index 000000000000..b1cf464f2345 --- /dev/null +++ b/test/AnalyzeView/MAVLinkConsoleControllerTest.cc @@ -0,0 +1,226 @@ +#include "MAVLinkConsoleControllerTest.h" + +#include +#include +#include + +#include "MAVLinkConsoleController.h" + +// --------------------------------------------------------------------------- +// Construction / initial state +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_constructionTest() +{ + // Should construct and destruct without crashing even with no active vehicle. + MAVLinkConsoleController controller; + QVERIFY(true); +} + +void MAVLinkConsoleControllerTest::_initialStateTest() +{ + MAVLinkConsoleController controller; + + // Model must start empty. + QCOMPARE(controller.rowCount(), 0); + + // text property must return an empty string when there are no rows. + const QString text = controller.property("text").toString(); + QCOMPARE(text, QString()); +} + +// --------------------------------------------------------------------------- +// CommandHistory – navigation on empty history +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyUpOnEmptyTest() +{ + MAVLinkConsoleController controller; + + // With no history, historyUp must echo back whatever was passed in. + const QString current = QStringLiteral("my input"); + QCOMPARE(controller.historyUp(current), current); +} + +void MAVLinkConsoleControllerTest::_historyDownOnEmptyTest() +{ + MAVLinkConsoleController controller; + + // With no history, historyDown must echo back whatever was passed in. + const QString current = QStringLiteral("my input"); + QCOMPARE(controller.historyDown(current), current); +} + +// --------------------------------------------------------------------------- +// CommandHistory – single entry +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyUpSingleEntryTest() +{ + MAVLinkConsoleController controller; + + // sendCommand appends non-empty lines to history even without a vehicle. + controller.sendCommand(QStringLiteral("ls")); + + // First up should yield the recorded command. + QCOMPARE(controller.historyUp(QStringLiteral("")), QStringLiteral("ls")); + + // A second up at the top boundary must return the same current string. + QCOMPARE(controller.historyUp(QStringLiteral("ls")), QStringLiteral("ls")); +} + +// --------------------------------------------------------------------------- +// CommandHistory – multi-entry up/down round-trip +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyUpDownNavigationTest() +{ + MAVLinkConsoleController controller; + + controller.sendCommand(QStringLiteral("cmd1")); + controller.sendCommand(QStringLiteral("cmd2")); + controller.sendCommand(QStringLiteral("cmd3")); + + // Navigate backwards: cmd3 -> cmd2 -> cmd1 + const QString r1 = controller.historyUp(QStringLiteral("")); + QCOMPARE(r1, QStringLiteral("cmd3")); + + const QString r2 = controller.historyUp(r1); + QCOMPARE(r2, QStringLiteral("cmd2")); + + const QString r3 = controller.historyUp(r2); + QCOMPARE(r3, QStringLiteral("cmd1")); + + // Navigate forward: cmd2 -> cmd3 -> empty (past end) + const QString f1 = controller.historyDown(r3); + QCOMPARE(f1, QStringLiteral("cmd2")); + + const QString f2 = controller.historyDown(f1); + QCOMPARE(f2, QStringLiteral("cmd3")); + + const QString f3 = controller.historyDown(f2); + QCOMPARE(f3, QStringLiteral("")); +} + +// --------------------------------------------------------------------------- +// CommandHistory – duplicate suppression +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyNoDuplicatesTest() +{ + MAVLinkConsoleController controller; + + controller.sendCommand(QStringLiteral("ping")); + controller.sendCommand(QStringLiteral("ping")); // duplicate – must not be appended + + // A single up should reach "ping". + const QString r1 = controller.historyUp(QStringLiteral("")); + QCOMPARE(r1, QStringLiteral("ping")); + + // Another up at the boundary must return the same string (only one entry). + const QString r2 = controller.historyUp(r1); + QCOMPARE(r2, QStringLiteral("ping")); +} + +// --------------------------------------------------------------------------- +// CommandHistory – boundary: already at top (index 0) +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyUpAtTopBoundaryTest() +{ + MAVLinkConsoleController controller; + + controller.sendCommand(QStringLiteral("alpha")); + + // Move to top. + controller.historyUp(QStringLiteral("")); + + // Another up must return current unchanged (boundary protection). + const QString current = QStringLiteral("alpha"); + QCOMPARE(controller.historyUp(current), current); +} + +// --------------------------------------------------------------------------- +// CommandHistory – boundary: already at bottom (index == history.length()) +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyDownAtBottomBoundaryTest() +{ + MAVLinkConsoleController controller; + + controller.sendCommand(QStringLiteral("beta")); + + // At the bottom (fresh after sendCommand), down must echo current. + const QString current = QStringLiteral("beta"); + QCOMPARE(controller.historyDown(current), current); +} + +// --------------------------------------------------------------------------- +// handleClipboard – no newline in combined string +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_handleClipboardNoNewlineTest() +{ + MAVLinkConsoleController controller; + + // Clear clipboard so the result is purely from command_pre. + QGuiApplication::clipboard()->clear(); + + const QString prefix = QStringLiteral("prefix"); + const QString result = controller.handleClipboard(prefix); + + // No newline present, so the whole string is returned as the pending line. + QCOMPARE(result, prefix); +} + +// --------------------------------------------------------------------------- +// handleClipboard – newline at the end of clipboard text +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_handleClipboardWithNewlineTest() +{ + MAVLinkConsoleController controller; + + // Place a newline-terminated string in the clipboard. + QGuiApplication::clipboard()->setText(QStringLiteral("clipped\n")); + + // With an empty prefix the clipboard text has a trailing newline. + // The part before the newline is sent as a command; the remainder + // (empty string after the newline) is returned. + const QString result = controller.handleClipboard(QStringLiteral("")); + QCOMPARE(result, QStringLiteral("")); +} + +// --------------------------------------------------------------------------- +// handleClipboard – empty prefix, clipboard with no newline +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_handleClipboardEmptyPrefixTest() +{ + MAVLinkConsoleController controller; + + QGuiApplication::clipboard()->setText(QStringLiteral("data")); + + // No newline -> entire combined string returned, nothing sent. + const QString result = controller.handleClipboard(QStringLiteral("")); + QCOMPARE(result, QStringLiteral("data")); +} + +// --------------------------------------------------------------------------- +// handleClipboard – multiple newlines, last line returned +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_handleClipboardMultilineTest() +{ + MAVLinkConsoleController controller; + + // Three lines: "a\nb\nc" — last line has no trailing newline. + QGuiApplication::clipboard()->setText(QStringLiteral("a\nb\nc")); + + // Combined string is "a\nb\nc". Last newline is between "b" and "c". + // "a\nb" is sent; "c" is returned. + const QString result = controller.handleClipboard(QStringLiteral("")); + QCOMPARE(result, QStringLiteral("c")); +} + +UT_REGISTER_TEST(MAVLinkConsoleControllerTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkConsoleControllerTest.h b/test/AnalyzeView/MAVLinkConsoleControllerTest.h new file mode 100644 index 000000000000..4b1335ce5e91 --- /dev/null +++ b/test/AnalyzeView/MAVLinkConsoleControllerTest.h @@ -0,0 +1,25 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkConsoleController; + +class MAVLinkConsoleControllerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _initialStateTest(); + void _historyUpOnEmptyTest(); + void _historyDownOnEmptyTest(); + void _historyUpSingleEntryTest(); + void _historyUpDownNavigationTest(); + void _historyNoDuplicatesTest(); + void _historyUpAtTopBoundaryTest(); + void _historyDownAtBottomBoundaryTest(); + void _handleClipboardNoNewlineTest(); + void _handleClipboardWithNewlineTest(); + void _handleClipboardEmptyPrefixTest(); + void _handleClipboardMultilineTest(); +}; diff --git a/test/AnalyzeView/MAVLinkInspectorControllerTest.cc b/test/AnalyzeView/MAVLinkInspectorControllerTest.cc new file mode 100644 index 000000000000..38de1393b605 --- /dev/null +++ b/test/AnalyzeView/MAVLinkInspectorControllerTest.cc @@ -0,0 +1,61 @@ +#include "MAVLinkInspectorControllerTest.h" + +#include "MAVLinkInspectorController.h" +#include "QmlObjectListModel.h" + +void MAVLinkInspectorControllerTest::_constructionTest() +{ + MAVLinkInspectorController controller; + + QVERIFY(controller.systems() != nullptr); + QVERIFY(controller.activeSystem() == nullptr); +} + +void MAVLinkInspectorControllerTest::_timeScalesNonEmptyTest() +{ + MAVLinkInspectorController controller; + + QVERIFY(!controller.timeScales().isEmpty()); +} + +void MAVLinkInspectorControllerTest::_rangeListNonEmptyTest() +{ + MAVLinkInspectorController controller; + + QVERIFY(!controller.rangeList().isEmpty()); +} + +void MAVLinkInspectorControllerTest::_systemNamesInitiallyEmptyTest() +{ + MAVLinkInspectorController controller; + + // No vehicles connected in unit test context + QVERIFY(controller.systemNames().isEmpty()); +} + +void MAVLinkInspectorControllerTest::_activeSystemInitiallyNullTest() +{ + MAVLinkInspectorController controller; + + QVERIFY(controller.activeSystem() == nullptr); +} + +void MAVLinkInspectorControllerTest::_timeScalesCountTest() +{ + MAVLinkInspectorController controller; + + // 4 time scales: 5s, 10s, 30s, 60s + QCOMPARE(controller.timeScales().count(), 4); + QCOMPARE(controller.timeScaleSt().count(), 4); +} + +void MAVLinkInspectorControllerTest::_rangeListCountTest() +{ + MAVLinkInspectorController controller; + + // 10 range entries: Auto + 9 fixed ranges + QCOMPARE(controller.rangeList().count(), 10); + QCOMPARE(controller.rangeSt().count(), 10); +} + +UT_REGISTER_TEST(MAVLinkInspectorControllerTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkInspectorControllerTest.h b/test/AnalyzeView/MAVLinkInspectorControllerTest.h new file mode 100644 index 000000000000..b519d64e9316 --- /dev/null +++ b/test/AnalyzeView/MAVLinkInspectorControllerTest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkInspectorControllerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _timeScalesNonEmptyTest(); + void _rangeListNonEmptyTest(); + void _systemNamesInitiallyEmptyTest(); + void _activeSystemInitiallyNullTest(); + void _timeScalesCountTest(); + void _rangeListCountTest(); +}; diff --git a/test/AnalyzeView/LogDownloadTest.cc b/test/AnalyzeView/MAVLinkLogDownloadTest.cc similarity index 80% rename from test/AnalyzeView/LogDownloadTest.cc rename to test/AnalyzeView/MAVLinkLogDownloadTest.cc index 4606f601534c..6905a7e9b0b2 100644 --- a/test/AnalyzeView/LogDownloadTest.cc +++ b/test/AnalyzeView/MAVLinkLogDownloadTest.cc @@ -1,18 +1,18 @@ -#include "LogDownloadTest.h" +#include "MAVLinkLogDownloadTest.h" #include -#include "LogDownloadController.h" -#include "LogEntry.h" +#include "MAVLinkLogController.h" +#include "MAVLinkLogEntry.h" #include "MAVLinkProtocol.h" #include "MultiSignalSpy.h" #include "MultiVehicleManager.h" #include "QmlObjectListModel.h" -void LogDownloadTest::_downloadTest() +void MAVLinkLogDownloadTest::_downloadTest() { // VehicleTest::init() already connects the mock link - LogDownloadController* const controller = new LogDownloadController(this); + MAVLinkLogController* const controller = new MAVLinkLogController(this); MultiSignalSpy* multiSpyLogDownloadController = new MultiSignalSpy(this); QVERIFY(multiSpyLogDownloadController->init(controller)); controller->refresh(); @@ -25,7 +25,7 @@ void LogDownloadTest::_downloadTest() multiSpyLogDownloadController->clearAllSignals(); QmlObjectListModel* const model = controller->_getModel(); QVERIFY(model); - model->value(0)->setSelected(true); + model->value(0)->setSelected(true); const QString downloadTo = QDir::currentPath(); controller->download(downloadTo); QVERIFY(multiSpyLogDownloadController->waitForSignal("downloadingLogsChanged", TestTimeout::longMs())); @@ -40,4 +40,4 @@ void LogDownloadTest::_downloadTest() (void)QFile::remove(downloadFile); } -UT_REGISTER_TEST(LogDownloadTest, TestLabel::Integration, TestLabel::AnalyzeView, TestLabel::Vehicle) +UT_REGISTER_TEST(MAVLinkLogDownloadTest, TestLabel::Integration, TestLabel::AnalyzeView, TestLabel::Vehicle) diff --git a/test/AnalyzeView/LogDownloadTest.h b/test/AnalyzeView/MAVLinkLogDownloadTest.h similarity index 69% rename from test/AnalyzeView/LogDownloadTest.h rename to test/AnalyzeView/MAVLinkLogDownloadTest.h index 75a25edfb2e5..4b8e48e5a11e 100644 --- a/test/AnalyzeView/LogDownloadTest.h +++ b/test/AnalyzeView/MAVLinkLogDownloadTest.h @@ -2,7 +2,7 @@ #include "BaseClasses/VehicleTest.h" -class LogDownloadTest : public VehicleTest +class MAVLinkLogDownloadTest : public VehicleTest { Q_OBJECT diff --git a/test/AnalyzeView/MAVLinkMessageFieldTest.cc b/test/AnalyzeView/MAVLinkMessageFieldTest.cc new file mode 100644 index 000000000000..d95fbb6bf28a --- /dev/null +++ b/test/AnalyzeView/MAVLinkMessageFieldTest.cc @@ -0,0 +1,127 @@ +#include "MAVLinkMessageFieldTest.h" + +#include + +#include "MAVLinkLib.h" +#include "MAVLinkMessage.h" +#include "MAVLinkMessageField.h" + +namespace { + +QGCMAVLinkMessage *makeHeartbeatMsg(QObject *parent = nullptr) +{ + mavlink_message_t msg{}; + mavlink_msg_heartbeat_pack_chan(1, 1, MAVLINK_COMM_0, &msg, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + return new QGCMAVLinkMessage(msg, parent); +} + +} // namespace + +void MAVLinkMessageFieldTest::_constructionTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + + // Pass nullptr as parent to avoid double-free (stack field + parent delete) + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QCOMPARE(field.name(), QStringLiteral("type")); + QCOMPARE(field.type(), QStringLiteral("uint8_t")); + + delete msg; +} + +void MAVLinkMessageFieldTest::_initialValueEmptyTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QVERIFY(field.value().isEmpty()); + + delete msg; +} + +void MAVLinkMessageFieldTest::_selectableDefaultsTrueTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QVERIFY(field.selectable()); + + delete msg; +} + +void MAVLinkMessageFieldTest::_setSelectableTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QSignalSpy selectableSpy(&field, &QGCMAVLinkMessageField::selectableChanged); + + field.setSelectable(false); + QVERIFY(!field.selectable()); + QCOMPARE(selectableSpy.count(), 1); + + // Same value must not emit + field.setSelectable(false); + QCOMPARE(selectableSpy.count(), 1); + + field.setSelectable(true); + QVERIFY(field.selectable()); + QCOMPARE(selectableSpy.count(), 2); + + delete msg; +} + +void MAVLinkMessageFieldTest::_updateValueChangesValueTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QSignalSpy valueSpy(&field, &QGCMAVLinkMessageField::valueChanged); + + field.updateValue(QStringLiteral("2"), 2.0); + QCOMPARE(field.value(), QStringLiteral("2")); + QCOMPARE(valueSpy.count(), 1); + + delete msg; +} + +void MAVLinkMessageFieldTest::_updateValueNoopOnSameValueTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + field.updateValue(QStringLiteral("5"), 5.0); + + QSignalSpy valueSpy(&field, &QGCMAVLinkMessageField::valueChanged); + + // Calling with the same string value must not emit valueChanged + field.updateValue(QStringLiteral("5"), 5.0); + QCOMPARE(valueSpy.count(), 0); + + delete msg; +} + +void MAVLinkMessageFieldTest::_labelFormatTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + QGCMAVLinkMessageField field(QStringLiteral("autopilot"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + // label() must follow the pattern ": " + const QString expected = QStringLiteral("HEARTBEAT: autopilot"); + QCOMPARE(field.label(), expected); + + delete msg; +} + +UT_REGISTER_TEST(MAVLinkMessageFieldTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkMessageFieldTest.h b/test/AnalyzeView/MAVLinkMessageFieldTest.h new file mode 100644 index 000000000000..ba21c7885a9d --- /dev/null +++ b/test/AnalyzeView/MAVLinkMessageFieldTest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkMessageFieldTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _initialValueEmptyTest(); + void _selectableDefaultsTrueTest(); + void _setSelectableTest(); + void _updateValueChangesValueTest(); + void _updateValueNoopOnSameValueTest(); + void _labelFormatTest(); +}; diff --git a/test/AnalyzeView/MAVLinkMessageTest.cc b/test/AnalyzeView/MAVLinkMessageTest.cc new file mode 100644 index 000000000000..41689a9cd9cd --- /dev/null +++ b/test/AnalyzeView/MAVLinkMessageTest.cc @@ -0,0 +1,128 @@ +#include "MAVLinkMessageTest.h" + +#include + +#include "MAVLinkLib.h" +#include "MAVLinkMessage.h" +#include "QmlObjectListModel.h" + +namespace { + +mavlink_message_t makeHeartbeat(uint8_t sysId = 1, uint8_t compId = 1) +{ + mavlink_message_t msg{}; + mavlink_msg_heartbeat_pack_chan( + sysId, compId, MAVLINK_COMM_0, &msg, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + return msg; +} + +} // namespace + +void MAVLinkMessageTest::_constructionTest() +{ + const mavlink_message_t msg = makeHeartbeat(2, 3); + QGCMAVLinkMessage message(msg); + + QCOMPARE(message.id(), static_cast(MAVLINK_MSG_ID_HEARTBEAT)); + QCOMPARE(message.sysId(), static_cast(2)); + QCOMPARE(message.compId(), static_cast(3)); + QCOMPARE(message.name(), QStringLiteral("HEARTBEAT")); +} + +void MAVLinkMessageTest::_countStartsAtOneTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + QCOMPARE(message.count(), static_cast(1)); +} + +void MAVLinkMessageTest::_updateIncrementsCountTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + QSignalSpy countSpy(&message, &QGCMAVLinkMessage::countChanged); + + message.update(msg); + QCOMPARE(message.count(), static_cast(2)); + QCOMPARE(countSpy.count(), 1); + + message.update(msg); + QCOMPARE(message.count(), static_cast(3)); + QCOMPARE(countSpy.count(), 2); +} + +void MAVLinkMessageTest::_setSelectedTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + QSignalSpy selectedSpy(&message, &QGCMAVLinkMessage::selectedChanged); + + QVERIFY(!message.selected()); + + message.setSelected(true); + QVERIFY(message.selected()); + QCOMPARE(selectedSpy.count(), 1); + + // Setting to the same value must not emit again + message.setSelected(true); + QCOMPARE(selectedSpy.count(), 1); + + message.setSelected(false); + QVERIFY(!message.selected()); + QCOMPARE(selectedSpy.count(), 2); +} + +void MAVLinkMessageTest::_fieldsPopulatedTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + const QmlObjectListModel *fields = message.fields(); + QVERIFY(fields != nullptr); + + // HEARTBEAT has 6 fields: type, autopilot, base_mode, custom_mode, system_status, mavlink_version + const mavlink_message_info_t *info = mavlink_get_message_info(&msg); + QVERIFY(info != nullptr); + QCOMPARE(fields->count(), static_cast(info->num_fields)); +} + +void MAVLinkMessageTest::_setTargetRateHzTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + QSignalSpy rateSpy(&message, &QGCMAVLinkMessage::targetRateHzChanged); + + QCOMPARE(message.targetRateHz(), static_cast(0)); + + message.setTargetRateHz(10); + QCOMPARE(message.targetRateHz(), static_cast(10)); + QCOMPARE(rateSpy.count(), 1); + + // Same value must not emit + message.setTargetRateHz(10); + QCOMPARE(rateSpy.count(), 1); +} + +void MAVLinkMessageTest::_updateFreqTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + // Initial frequency is zero + QCOMPARE_FUZZY(message.actualRateHz(), 0.0, 1e-9); + + // Simulate one update since last freq refresh + message.update(msg); + message.updateFreq(); + + // _count started at 1, update() made it 2, _lastCount was 0 + // msgCount = 2 - 0 = 2; EWMA = 0.2*0 + 0.8*2 = 1.6 + QCOMPARE_FUZZY(message.actualRateHz(), 1.6, 1e-9); +} + +UT_REGISTER_TEST(MAVLinkMessageTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkMessageTest.h b/test/AnalyzeView/MAVLinkMessageTest.h new file mode 100644 index 000000000000..caa223c8fcca --- /dev/null +++ b/test/AnalyzeView/MAVLinkMessageTest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkMessageTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _countStartsAtOneTest(); + void _updateIncrementsCountTest(); + void _setSelectedTest(); + void _fieldsPopulatedTest(); + void _setTargetRateHzTest(); + void _updateFreqTest(); +}; diff --git a/test/AnalyzeView/MAVLinkSystemTest.cc b/test/AnalyzeView/MAVLinkSystemTest.cc new file mode 100644 index 000000000000..c76f1f0f1923 --- /dev/null +++ b/test/AnalyzeView/MAVLinkSystemTest.cc @@ -0,0 +1,168 @@ +#include "MAVLinkSystemTest.h" + +#include + +#include "MAVLinkLib.h" +#include "MAVLinkMessage.h" +#include "MAVLinkSystem.h" +#include "QmlObjectListModel.h" + +namespace { + +QGCMAVLinkMessage *makeHeartbeatMsg(uint8_t sysId = 1, uint8_t compId = 1, QObject *parent = nullptr) +{ + mavlink_message_t msg{}; + mavlink_msg_heartbeat_pack_chan(sysId, compId, MAVLINK_COMM_0, &msg, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + return new QGCMAVLinkMessage(msg, parent); +} + +} // namespace + +void MAVLinkSystemTest::_constructionTest() +{ + QGCMAVLinkSystem system(7); + + QCOMPARE(system.id(), static_cast(7)); +} + +void MAVLinkSystemTest::_initiallyEmptyMessagesTest() +{ + QGCMAVLinkSystem system(1); + + QVERIFY(system.messages() != nullptr); + QCOMPARE(system.messages()->count(), 0); +} + +void MAVLinkSystemTest::_initiallyNoCompIDsTest() +{ + QGCMAVLinkSystem system(1); + + QVERIFY(system.compIDs().isEmpty()); + QVERIFY(system.compIDsStr().isEmpty()); +} + +void MAVLinkSystemTest::_appendAndFindMessageTest() +{ + QGCMAVLinkSystem system(1); + + QGCMAVLinkMessage *msg = makeHeartbeatMsg(1, 1); + system.append(msg); + + QCOMPARE(system.messages()->count(), 1); + + QGCMAVLinkMessage *found = system.findMessage(MAVLINK_MSG_ID_HEARTBEAT, 1); + QVERIFY(found != nullptr); + QCOMPARE(found->id(), static_cast(MAVLINK_MSG_ID_HEARTBEAT)); + QCOMPARE(found->compId(), static_cast(1)); +} + +void MAVLinkSystemTest::_findMessageNotFoundTest() +{ + QGCMAVLinkSystem system(1); + + // findMessage on an empty system returns nullptr + QGCMAVLinkMessage *found = system.findMessage(MAVLINK_MSG_ID_HEARTBEAT, 1); + QVERIFY(found == nullptr); + + // findMessage with a wrong compId also returns nullptr + QGCMAVLinkMessage *msg = makeHeartbeatMsg(1, 1); + system.append(msg); + + QGCMAVLinkMessage *wrongComp = system.findMessage(MAVLINK_MSG_ID_HEARTBEAT, 99); + QVERIFY(wrongComp == nullptr); +} + +void MAVLinkSystemTest::_findMessageByPointerTest() +{ + QGCMAVLinkSystem system(1); + + QGCMAVLinkMessage *msg = makeHeartbeatMsg(1, 1); + system.append(msg); + + const int idx = system.findMessage(msg); + QCOMPARE(idx, 0); + + // A pointer that was never appended returns -1 + QGCMAVLinkMessage fakeMsg(mavlink_message_t{}); + QCOMPARE(system.findMessage(&fakeMsg), -1); +} + +void MAVLinkSystemTest::_setSelectedAndSelectedMsgTest() +{ + QGCMAVLinkSystem system(1); + + // Two messages with different msg IDs so they sort differently + mavlink_message_t raw1{}; + mavlink_msg_heartbeat_pack_chan(1, 1, MAVLINK_COMM_0, &raw1, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + auto *msg1 = new QGCMAVLinkMessage(raw1); + + // Use a second heartbeat with a different compId so they coexist + mavlink_message_t raw2{}; + mavlink_msg_heartbeat_pack_chan(1, 2, MAVLINK_COMM_0, &raw2, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + auto *msg2 = new QGCMAVLinkMessage(raw2); + + system.append(msg1); + system.append(msg2); + + QSignalSpy selectedSpy(&system, &QGCMAVLinkSystem::selectedChanged); + + system.setSelected(0); + QVERIFY(selectedSpy.count() >= 1); + + QGCMAVLinkMessage *sel = system.selectedMsg(); + QVERIFY(sel != nullptr); + // The selected message must be the one at index 0 of the sorted list + QCOMPARE(system.findMessage(sel), 0); +} + +void MAVLinkSystemTest::_setSelectedOutOfBoundsTest() +{ + QGCMAVLinkSystem system(1); + + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + system.append(msg); + + QSignalSpy selectedSpy(&system, &QGCMAVLinkSystem::selectedChanged); + + // Index equal to count is out of bounds - must be silently ignored + system.setSelected(1); + QCOMPARE(selectedSpy.count(), 0); +} + +void MAVLinkSystemTest::_appendUpdatesCompIDsTest() +{ + QGCMAVLinkSystem system(1); + + QSignalSpy compSpy(&system, &QGCMAVLinkSystem::compIDsChanged); + + QGCMAVLinkMessage *msg = makeHeartbeatMsg(1, 42); + system.append(msg); + + QCOMPARE(compSpy.count(), 1); + QVERIFY(system.compIDs().contains(42)); + QVERIFY(!system.compIDsStr().isEmpty()); + + // Appending another message with the same compId must not emit again + QGCMAVLinkMessage *msg2 = makeHeartbeatMsg(1, 42); + // Give it a different msgid so it is a distinct entry - use a raw msg + mavlink_message_t raw{}; + mavlink_msg_heartbeat_pack_chan(1, 42, MAVLINK_COMM_1, &raw, + MAV_TYPE_FIXED_WING, MAV_AUTOPILOT_GENERIC, 0, 0, MAV_STATE_ACTIVE); + auto *msg3 = new QGCMAVLinkMessage(raw); + delete msg2; // not used + system.append(msg3); + + QCOMPARE(compSpy.count(), 1); +} + +void MAVLinkSystemTest::_selectedMsgOnEmptySystemTest() +{ + QGCMAVLinkSystem system(1); + + QVERIFY(system.selectedMsg() == nullptr); +} + +UT_REGISTER_TEST(MAVLinkSystemTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkSystemTest.h b/test/AnalyzeView/MAVLinkSystemTest.h new file mode 100644 index 000000000000..f4808f42da78 --- /dev/null +++ b/test/AnalyzeView/MAVLinkSystemTest.h @@ -0,0 +1,20 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkSystemTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _initiallyEmptyMessagesTest(); + void _initiallyNoCompIDsTest(); + void _appendAndFindMessageTest(); + void _findMessageNotFoundTest(); + void _findMessageByPointerTest(); + void _setSelectedAndSelectedMsgTest(); + void _setSelectedOutOfBoundsTest(); + void _appendUpdatesCompIDsTest(); + void _selectedMsgOnEmptySystemTest(); +}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 53de62a8f5d6..d18cb7d37b6e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -73,7 +73,13 @@ add_qgc_test(GeoTagControllerTest LABELS Unit AnalyzeView RESOURCE_LOCK TempFile add_qgc_test(GeoTagDataTest LABELS Unit AnalyzeView) add_qgc_test(GeoTagImageModelTest LABELS Unit AnalyzeView) add_qgc_test(ULogParserTest LABELS Unit AnalyzeView RESOURCE_LOCK TempFiles) -add_qgc_test(LogDownloadTest LABELS Integration Vehicle RESOURCE_LOCK MockLink) +add_qgc_test(MAVLinkChartControllerTest LABELS Unit AnalyzeView) +add_qgc_test(MAVLinkConsoleControllerTest LABELS Unit AnalyzeView) +add_qgc_test(MAVLinkInspectorControllerTest LABELS Unit AnalyzeView) +add_qgc_test(MAVLinkLogDownloadTest LABELS Integration AnalyzeView Vehicle RESOURCE_LOCK MockLink) +add_qgc_test(MAVLinkMessageFieldTest LABELS Unit AnalyzeView) +add_qgc_test(MAVLinkMessageTest LABELS Unit AnalyzeView) +add_qgc_test(MAVLinkSystemTest LABELS Unit AnalyzeView) add_qgc_test(MavlinkLogTest LABELS Integration AnalyzeView Vehicle RESOURCE_LOCK MockLink) # ---------------------------------------------------------------------------- From 4ce944703aed76404425324041f06afa6c6cb33e Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Wed, 11 Mar 2026 05:19:23 -0400 Subject: [PATCH 065/119] ci: overhaul CI workflows, rewrite shell scripts to Python, consolidate shared modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI/CD workflow changes: - Wire detect-changes action into all platform workflows replacing static paths: filters; add resources/ and *.qrc to shared detection patterns - Add event filter to artifact collection and download so build-results correctly distinguishes PR vs push runs - Remove paths: filters from pre-commit workflow; extract precommit_results.py - Extract plan_docker_builds.py from inline docker workflow matrix logic - Extract test_duration_report.py from inline test-duration-report action - Add Python dependency patterns to dependency-review workflow - Consolidate crowdin workflow from 2 jobs to 1 with mode detection - Update aws-actions/configure-aws-credentials v4→v6 (Node.js 24) - Update lukka/get-cmake v4.2.1→v4.2.3 - Add OIDC support to gstreamer action for AWS credential management - Move ccache version to build-config.json for Renovate management - Remove redundant Qt cache layer (install-qt-action handles caching) - Uncomment CODEOWNERS rules, enable harden-runner, update bug template Shell→Python rewrites: - Replace check-deps.sh → check_deps.py with apt-cache validation - Replace coverage.sh → coverage.py with proper test failure tracking - Replace generate-docs.sh → generate_docs.py (no Doxyfile mutation) - Replace pre-commit.sh → pre_commit.py - Create shared shell-utils.sh for remaining shell scripts (clean.sh, release.sh, profile.sh, run-arducopter-sitl.sh) Shared module consolidation: - Add common/build_config.py: find, load, get, export build-config.json values; replace duplicated config reading across scripts - Add common/github_runs.py: select_latest_runs_by_name, group_runs_by_name; replace duplicated run selection logic and delete workflow_runs.py wrapper - Expand ccache_helper.py with subcommands (scope, config, windows-config, install-windows, configure-env, configure-cpm-cache, fingerprint) replacing inline bash in cache/action.yml - Add resolve_gstreamer_config.py replacing inline config in gstreamer action - Add ci_bootstrap.py and setup_bootstrap.py for consistent cross-directory Python imports; remove sys.path.insert boilerplate from all entry points Tools infrastructure: - Add tools/pyproject.toml with project metadata, optional-dependency groups (scripts, precommit, test, ci), and pytest pythonpath config - Add uv.lock for reproducible dependency management - Move defusedxml from requirements.txt to pyproject.toml optional-deps - Refactor install_python.py to read package groups from pyproject.toml - Add --print-available-packages and --validate-extra-packages to install_dependencies.py - Remove sys.path.insert boilerplate from 6 test files (pyproject.toml pythonpath handles it) Bug fixes: - Fix profile.sh REPO_ROOT path resolution - Fix analyze.py version parsing - Fix shebangs in qgc-lupdate-json.py and run-arducopter-sitl.sh --- .github/CODEOWNERS | 7 +- .github/CONTRIBUTING.md | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/actions/aws-upload/action.yml | 4 +- .github/actions/benchmark-runner/action.yml | 11 +- .github/actions/build-config/action.yml | 3 + .github/actions/cache/action.yml | 162 +-- .github/actions/cmake-build/action.yml | 15 +- .github/actions/cmake-configure/action.yml | 10 +- .../actions/collect-artifact-sizes/action.yml | 8 + .github/actions/common/action.yml | 5 +- .github/actions/coverage/action.yml | 38 +- .github/actions/detect-changes/action.yml | 30 +- .../actions/download-all-artifacts/action.yml | 8 + .github/actions/gstreamer/action.yml | 56 +- .../actions/install-dependencies/action.yml | 26 +- .github/actions/qt-install/action.yml | 22 - .github/actions/setup-python/action.yml | 14 +- .github/actions/size-analysis/action.yml | 10 +- .../actions/test-duration-report/action.yml | 165 +-- .github/actions/verify-executable/action.yml | 11 +- .github/build-config.json | 1 + .github/codecov.yml | 12 +- .github/dependabot.yml | 5 +- .github/renovate.json | 1 + .github/scripts/README.md | 7 +- .github/scripts/ccache_helper.py | 256 +++- .github/scripts/check_baseline_ready.py | 17 +- .github/scripts/ci_bootstrap.py | 16 + .github/scripts/collect_artifact_sizes.py | 73 +- .github/scripts/collect_build_status.py | 26 +- .github/scripts/coverage_comment.py | 19 +- .github/scripts/find_binary.py | 2 +- .../scripts/generate_build_results_comment.py | 19 +- .github/scripts/plan_docker_builds.py | 82 ++ .github/scripts/precommit_results.py | 79 ++ .github/scripts/test_duration_report.py | 238 ++++ .github/scripts/tests/test_ccache_helper.py | 68 + .../tests/test_collect_artifact_sizes.py | 17 +- .../tests/test_collect_build_status.py | 2 +- .../scripts/tests/test_plan_docker_builds.py | 21 + .../scripts/tests/test_precommit_results.py | 34 + .../tests/test_test_duration_report.py | 52 + .github/scripts/tests/test_workflow_runs.py | 31 - .github/scripts/workflow_runs.py | 31 - .github/scripts/xml_utils.py | 27 + .github/trivy.yaml | 1 - .github/workflows/android.yml | 33 +- .github/workflows/build-gstreamer.yml | 2 + .github/workflows/build-results.yml | 31 +- .github/workflows/ci-scripts.yml | 4 + .github/workflows/crowdin.yml | 54 +- .github/workflows/dependency-review.yml | 6 + .github/workflows/docker.yml | 95 +- .github/workflows/linux.yml | 36 +- .github/workflows/macos.yml | 31 +- .github/workflows/performance.yml | 2 + .github/workflows/pre-commit.yml | 50 +- .github/workflows/release.yml | 8 +- .github/workflows/windows.yml | 40 +- .yamllint.yml | 9 + THREAD_SAFETY_ANALYSIS.md | 0 justfile | 6 +- tools/README.md | 40 +- tools/_bootstrap.py | 18 + tools/analyze.py | 3 +- tools/check-deps.sh | 261 ---- tools/check_deps.py | 256 ++++ tools/clean.sh | 13 +- tools/common/__init__.py | 26 + tools/common/build_config.py | 120 ++ tools/common/gh_actions.py | 10 +- tools/common/github_runs.py | 90 ++ tools/common/shell-utils.sh | 15 + tools/coverage.py | 216 +++ tools/coverage.sh | 185 --- tools/debuggers/profile.sh | 19 +- tools/generate-docs.sh | 239 ---- tools/generate_docs.py | 152 ++ tools/param-docs.py | 2 +- tools/pre-commit.sh | 228 --- tools/pre_commit.py | 206 +++ tools/pyproject.toml | 41 + tools/release.sh | 12 +- tools/setup/build-gstreamer.py | 64 +- tools/setup/download_artifacts.py | 125 +- tools/setup/install_dependencies.py | 85 +- tools/setup/install_python.py | 154 +- tools/setup/read_config.py | 104 +- tools/setup/resolve_gstreamer_config.py | 60 + tools/setup/setup_bootstrap.py | 14 + tools/simulation/mock_vehicle.py | 2 +- tools/simulation/run-arducopter-sitl.sh | 6 +- tools/tests/test_common.py | 6 - tools/tests/test_coverage.py | 12 + tools/tests/test_download_artifacts.py | 39 +- tools/tests/test_factgroup_generator.py | 7 +- tools/tests/test_generate_docs.py | 16 + tools/tests/test_gh_actions.py | 5 - tools/tests/test_install_dependencies.py | 25 +- tools/tests/test_install_python.py | 27 +- tools/tests/test_pre_commit.py | 33 + tools/tests/test_resolve_gstreamer_config.py | 19 + tools/translations/qgc-lupdate-json.py | 2 +- tools/uv.lock | 1256 +++++++++++++++++ 105 files changed, 4207 insertions(+), 2159 deletions(-) create mode 100644 .github/scripts/ci_bootstrap.py create mode 100644 .github/scripts/plan_docker_builds.py create mode 100644 .github/scripts/precommit_results.py create mode 100644 .github/scripts/test_duration_report.py create mode 100644 .github/scripts/tests/test_plan_docker_builds.py create mode 100644 .github/scripts/tests/test_precommit_results.py create mode 100644 .github/scripts/tests/test_test_duration_report.py delete mode 100644 .github/scripts/tests/test_workflow_runs.py delete mode 100644 .github/scripts/workflow_runs.py create mode 100644 .github/scripts/xml_utils.py create mode 100644 .yamllint.yml delete mode 100644 THREAD_SAFETY_ANALYSIS.md create mode 100644 tools/_bootstrap.py delete mode 100755 tools/check-deps.sh create mode 100644 tools/check_deps.py create mode 100644 tools/common/build_config.py create mode 100644 tools/common/github_runs.py create mode 100644 tools/common/shell-utils.sh create mode 100644 tools/coverage.py delete mode 100755 tools/coverage.sh delete mode 100755 tools/generate-docs.sh create mode 100644 tools/generate_docs.py delete mode 100755 tools/pre-commit.sh create mode 100644 tools/pre_commit.py create mode 100644 tools/pyproject.toml create mode 100644 tools/setup/resolve_gstreamer_config.py create mode 100644 tools/setup/setup_bootstrap.py create mode 100644 tools/tests/test_coverage.py create mode 100644 tools/tests/test_generate_docs.py create mode 100644 tools/tests/test_pre_commit.py create mode 100644 tools/tests/test_resolve_gstreamer_config.py create mode 100644 tools/uv.lock diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fe988a0c97ec..cb99286f0fda 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,5 +11,8 @@ # *.qml @DonLakeFlyer # Build system -# CMakeLists.txt @HTRamsey -# /cmake/ @HTRamsey +CMakeLists.txt @HTRamsey +/cmake/ @HTRamsey + +# CI/CD +/.github/ @HTRamsey diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fd7ae9286829..b3ea2120847d 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -89,7 +89,7 @@ QGroundControl uses [Crowdin](https://crowdin.com/project/qgroundcontrol) for co ```bash git add . - git commit -m "Add feature: brief description" + git commit -m "feat: brief description" ``` 6. **Push to your fork** diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a81ac38a42a5..4f9d9b726a9a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -79,7 +79,7 @@ body: label: System Information description: | Please include the following information: - - QGC Version: [e.g. 4.4.0] + - QGC Version: [e.g. 5.0.0] - QGC build: [e.g. daily, stable, self-built from source, etc...] - Operating System: [e.g. Windows 11, Ubuntu 22.04, macOS 15, iOS 17 ] - Flight Controller: [e.g. CubePilot Cube Orange, Pixhawk 6X, etc.] diff --git a/.github/actions/aws-upload/action.yml b/.github/actions/aws-upload/action.yml index 6aa7fa8de200..41eaa952242a 100644 --- a/.github/actions/aws-upload/action.yml +++ b/.github/actions/aws-upload/action.yml @@ -46,14 +46,14 @@ runs: - name: Configure AWS Credentials (OIDC) if: inputs.aws_role_arn != '' - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ inputs.aws_role_arn }} aws-region: ${{ inputs.aws_region }} - name: Configure AWS Credentials (Static) if: inputs.aws_role_arn == '' && inputs.aws_key_id != '' - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: aws-access-key-id: ${{ inputs.aws_key_id }} aws-secret-access-key: ${{ inputs.aws_secret_access_key }} diff --git a/.github/actions/benchmark-runner/action.yml b/.github/actions/benchmark-runner/action.yml index 195659e38903..3afa6b55d995 100644 --- a/.github/actions/benchmark-runner/action.yml +++ b/.github/actions/benchmark-runner/action.yml @@ -26,16 +26,7 @@ runs: INPUT_OUTPUT_FILE: ${{ inputs.output-file }} INPUT_QT_PLATFORM: ${{ inputs.qt-platform }} run: | - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - - "${py_cmd}" "${GITHUB_WORKSPACE}/.github/scripts/benchmark_runner.py" \ + python3 "${GITHUB_WORKSPACE}/.github/scripts/benchmark_runner.py" \ --binary "$INPUT_BINARY_PATH" \ --filter "$INPUT_TEST_FILTER" \ --output "$INPUT_OUTPUT_FILE" \ diff --git a/.github/actions/build-config/action.yml b/.github/actions/build-config/action.yml index ecba81a47b8d..91abbb710785 100644 --- a/.github/actions/build-config/action.yml +++ b/.github/actions/build-config/action.yml @@ -23,6 +23,9 @@ outputs: gstreamer_windows_version: description: GStreamer version (Windows) value: ${{ steps.read.outputs.gstreamer_windows_version }} + ndk_version: + description: Android NDK version (short form) + value: ${{ steps.read.outputs.ndk_version }} ndk_full_version: description: Android NDK version (full form for sdkmanager) value: ${{ steps.read.outputs.ndk_full_version }} diff --git a/.github/actions/cache/action.yml b/.github/actions/cache/action.yml index d66137cda141..712356f2df02 100644 --- a/.github/actions/cache/action.yml +++ b/.github/actions/cache/action.yml @@ -27,26 +27,7 @@ runs: EVENT_NAME: ${{ github.event_name }} REF_NAME: ${{ github.ref_name }} PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - scope="shared" - case "${EVENT_NAME}" in - pull_request) - scope="pr-${PR_NUMBER:-unknown}" - ;; - workflow_dispatch) - scope="manual-${REF_NAME}" - ;; - push) - if [[ "${REF_NAME}" != "master" ]]; then - scope="branch-${REF_NAME}" - fi - ;; - *) - scope="${EVENT_NAME}-${REF_NAME}" - ;; - esac - scope="${scope//\//-}" - echo "scope=${scope}" >> "$GITHUB_OUTPUT" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" scope --event-name "${EVENT_NAME}" --ref-name "${REF_NAME}" --pr-number "${PR_NUMBER:-}" - name: Get ccache config id: ccache-config @@ -81,30 +62,14 @@ runs: fi ccache --version - - name: Setup sccache (Windows x64) - if: runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64' - uses: mozilla-actions/sccache-action@v0.0.9 - - - name: Enable sccache GHA cache backend - if: runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64' - shell: bash - run: echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" - - name: Determine Windows ccache arch - if: runner.os == 'Windows' && (inputs.target == 'android' || inputs.host == 'windows_arm64') + if: runner.os == 'Windows' id: win-ccache shell: bash - run: | - if [[ "${{ inputs.host }}" == "windows_arm64" ]]; then - echo "arch=aarch64" >> "$GITHUB_OUTPUT" - echo "sha256=5907c8f74dcfff920ee57df55a5cb98164630665926b2ba9085e9f5fb701c58f" >> "$GITHUB_OUTPUT" - else - echo "arch=x86_64" >> "$GITHUB_OUTPUT" - echo "sha256=1c78a0b816a3174d4b170b96294e016a21fb4a577dfd8361e7322f77f85c6348" >> "$GITHUB_OUTPUT" - fi + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" windows-config --host "${{ inputs.host }}" --target "${{ inputs.target }}" - name: Cache ccache binary (Windows) - if: runner.os == 'Windows' && (inputs.target == 'android' || inputs.host == 'windows_arm64') + if: runner.os == 'Windows' id: ccache-binary-win uses: actions/cache@v5 with: @@ -112,58 +77,30 @@ runs: key: ccache-binary-${{ steps.ccache-config.outputs.version }}-windows-${{ steps.win-ccache.outputs.arch }} - name: Install ccache (Windows) - if: runner.os == 'Windows' && (inputs.target == 'android' || inputs.host == 'windows_arm64') && steps.ccache-binary-win.outputs.cache-hit != 'true' + if: runner.os == 'Windows' && steps.ccache-binary-win.outputs.cache-hit != 'true' shell: bash env: CCACHE_VERSION: ${{ steps.ccache-config.outputs.version }} CCACHE_ARCH: ${{ steps.win-ccache.outputs.arch }} - # Update hashes when bumping CCACHE_VERSION CCACHE_WIN_SHA256: ${{ steps.win-ccache.outputs.sha256 }} run: | - set -euo pipefail - ccache_url="https://github.com/ccache/ccache/releases/download/v${CCACHE_VERSION}/ccache-${CCACHE_VERSION}-windows-${CCACHE_ARCH}.zip" - temp_dir="$(mktemp -d)" - temp_root="${RUNNER_TEMP//\\//}" - install_dir="${temp_root}/ccache-${CCACHE_VERSION}-windows-${CCACHE_ARCH}" - - curl --fail --location --silent --show-error "$ccache_url" -o "${temp_dir}/ccache.zip" - actual=$(python3 -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "${temp_dir}/ccache.zip") - if [[ "$actual" != "${CCACHE_WIN_SHA256}" ]]; then - echo "SHA256 mismatch: ${actual} != ${CCACHE_WIN_SHA256}" >&2 - exit 1 - fi - echo "SHA256 verified" - unzip -q "${temp_dir}/ccache.zip" -d "$temp_dir" - mkdir -p "$install_dir" - cp "${temp_dir}/ccache-${CCACHE_VERSION}-windows-${CCACHE_ARCH}/ccache.exe" "${install_dir}/ccache.exe" + python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" install-windows \ + --version "${CCACHE_VERSION}" \ + --arch "${CCACHE_ARCH}" \ + --sha256 "${CCACHE_WIN_SHA256}" \ + --runner-temp "${RUNNER_TEMP//\\//}" - name: Add ccache to PATH (Windows) - if: runner.os == 'Windows' && (inputs.target == 'android' || inputs.host == 'windows_arm64') + if: runner.os == 'Windows' shell: bash env: CCACHE_VERSION: ${{ steps.ccache-config.outputs.version }} CCACHE_ARCH: ${{ steps.win-ccache.outputs.arch }} - run: | - set -euo pipefail - temp_root="${RUNNER_TEMP//\\//}" - install_dir="${temp_root}/ccache-${CCACHE_VERSION}-windows-${CCACHE_ARCH}" - if [ ! -f "${install_dir}/ccache.exe" ]; then - echo "Error: ccache.exe not found at ${install_dir}/ccache.exe" >&2 - exit 1 - fi - - echo "$install_dir" >> "$GITHUB_PATH" - "${install_dir}/ccache.exe" --version + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" add-windows-path --version "${CCACHE_VERSION}" --arch "${CCACHE_ARCH}" --runner-temp "${RUNNER_TEMP//\\//}" - name: Configure ccache environment - if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64')" shell: bash - run: | - workspace_path="${GITHUB_WORKSPACE//\\//}" - echo "CCACHE_DIR=${workspace_path}/.ccache" >> "$GITHUB_ENV" - echo "CCACHE_BASEDIR=${workspace_path}" >> "$GITHUB_ENV" - echo "CCACHE_CONFIGPATH=${workspace_path}/tools/configs/ccache.conf" >> "$GITHUB_ENV" - mkdir -p "${workspace_path}/.ccache" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" configure-env --workspace "${GITHUB_WORKSPACE//\\//}" - name: Configure CPM source cache if: inputs.cpm-modules != '' @@ -171,72 +108,16 @@ runs: shell: bash env: CPM_MODULES_INPUT: ${{ inputs.cpm-modules }} - run: | - cpm_cache_path="${CPM_MODULES_INPUT//\\//}" - mkdir -p "${cpm_cache_path}" - echo "CPM_SOURCE_CACHE=${cpm_cache_path}" >> "$GITHUB_ENV" - echo "path=${cpm_cache_path}" >> "$GITHUB_OUTPUT" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" configure-cpm-cache --path "${CPM_MODULES_INPUT}" - name: Compute CPM dependency fingerprint if: inputs.cpm-modules != '' id: cpm-key shell: bash - run: | - set -euo pipefail - fingerprint="$(python3 - <<'PY' - import hashlib - import re - from pathlib import Path - - root = Path(".") - declaration_re = re.compile(r"CPM(Add|Find)Package|FetchContent_Declare") - - candidates: list[Path] = [] - for exact in [Path("CMakeLists.txt"), Path("cmake/modules/CPM.cmake"), Path(".github/build-config.json")]: - if exact.exists(): - candidates.append(exact) - - if (root / "cmake").exists(): - candidates.extend((root / "cmake").rglob("*.cmake")) - if (root / "src").exists(): - candidates.extend((root / "src").rglob("CMakeLists.txt")) - if (root / "test").exists(): - candidates.extend((root / "test").rglob("CMakeLists.txt")) - - dep_files: list[Path] = [] - for path in candidates: - if not path.is_file(): - continue - if path.name == "build-config.json": - dep_files.append(path) - continue - try: - text = path.read_text(encoding="utf-8", errors="ignore") - except OSError: - continue - if declaration_re.search(text): - dep_files.append(path) - - paths = sorted({str(path.as_posix()) for path in dep_files}) - h = hashlib.sha256() - for rel in paths: - path = Path(rel) - try: - content = path.read_bytes() - except OSError: - continue - h.update(rel.encode("utf-8")) - h.update(b"\0") - h.update(content) - h.update(b"\0") - - print(h.hexdigest()) - PY - )" - echo "fingerprint=${fingerprint}" >> "$GITHUB_OUTPUT" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" fingerprint --root "${GITHUB_WORKSPACE}" - name: Restore Build Cache (ccache, save) - if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64') && inputs.save-cache == 'true'" + if: inputs.save-cache == 'true' uses: actions/cache@v5 with: path: ${{ github.workspace }}/.ccache @@ -245,11 +126,9 @@ runs: ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}- ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-shared- ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}- - ccache-${{ inputs.host }}-${{ inputs.target }}- - ccache-${{ inputs.host }}- - name: Restore Build Cache (ccache, read-only) - if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64') && inputs.save-cache != 'true'" + if: inputs.save-cache != 'true' uses: actions/cache/restore@v5 with: path: ${{ github.workspace }}/.ccache @@ -258,15 +137,10 @@ runs: ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}- ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-shared- ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}- - ccache-${{ inputs.host }}-${{ inputs.target }}- - ccache-${{ inputs.host }}- - name: Initialize ccache stats - if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64')" shell: bash - run: | - ccache -z - echo "CCACHE_START_TIME=$(date +%s)" >> "$GITHUB_ENV" + run: ccache -z - name: Cache CPM Modules (restore and save) if: inputs.cpm-modules != '' && inputs.save-cache == 'true' diff --git a/.github/actions/cmake-build/action.yml b/.github/actions/cmake-build/action.yml index 672e5b358c0c..3b0eb0c4744d 100644 --- a/.github/actions/cmake-build/action.yml +++ b/.github/actions/cmake-build/action.yml @@ -132,19 +132,6 @@ runs: shell: bash run: | if command -v ccache >/dev/null 2>&1; then - # Evict entries not touched during this job - if [[ -n "${CCACHE_START_TIME:-}" ]]; then - elapsed=$(( $(date +%s) - CCACHE_START_TIME )) - if [[ $elapsed -gt 0 ]]; then - echo "Evicting ccache entries older than ${elapsed}s" - ccache --evict-older-than "${elapsed}s" || true - fi - fi - - # Job summary: hit/miss table + verbose stats (ccache 4.10+) + ccache -c python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" summary - elif command -v sccache >/dev/null 2>&1; then - echo "::group::sccache statistics" - sccache --show-stats - echo "::endgroup::" fi diff --git a/.github/actions/cmake-configure/action.yml b/.github/actions/cmake-configure/action.yml index 7aaa9b156dbd..3e94b63f6aff 100644 --- a/.github/actions/cmake-configure/action.yml +++ b/.github/actions/cmake-configure/action.yml @@ -95,12 +95,4 @@ runs: args+=("${extra[@]}") fi - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - "${py_cmd}" "${WORKSPACE}/tools/configure.py" "${args[@]}" + python3 "${WORKSPACE}/tools/configure.py" "${args[@]}" diff --git a/.github/actions/collect-artifact-sizes/action.yml b/.github/actions/collect-artifact-sizes/action.yml index 6f0e6882247f..9118c7b0e29c 100644 --- a/.github/actions/collect-artifact-sizes/action.yml +++ b/.github/actions/collect-artifact-sizes/action.yml @@ -9,6 +9,10 @@ inputs: description: Comma-separated workflow names to collect artifact sizes for required: false default: Linux,Windows,MacOS,Android + event: + description: Optional workflow event name to filter runs by + required: false + default: '' output-file: description: Output JSON file path required: true @@ -34,11 +38,15 @@ runs: REPO: ${{ github.repository }} HEAD_SHA: ${{ inputs.head-sha }} WORKFLOWS: ${{ inputs.workflows }} + EVENT: ${{ inputs.event }} OUTPUT_FILE: ${{ inputs.output-file }} RUNS_FILE: ${{ inputs.runs-file }} ARTIFACTS_FILE: ${{ inputs.artifacts-file }} run: | ARGS=(--repo "${REPO}" --head-sha "${HEAD_SHA}" --platform-workflows "${WORKFLOWS}" --output-file "${OUTPUT_FILE}") + if [[ -n "${EVENT}" ]]; then + ARGS+=(--event "${EVENT}") + fi if [[ -n "${RUNS_FILE}" && -f "${RUNS_FILE}" ]]; then ARGS+=(--runs-file "${RUNS_FILE}") fi diff --git a/.github/actions/common/action.yml b/.github/actions/common/action.yml index 9b2e08a62774..b08732e7b106 100644 --- a/.github/actions/common/action.yml +++ b/.github/actions/common/action.yml @@ -15,11 +15,12 @@ runs: remove_packages_one_command: true rm_cmd: rmz - - uses: lukka/get-cmake@v4.2.1 + - uses: lukka/get-cmake@v4.2.3 - - uses: actions/setup-python@v6 + - uses: ./.github/actions/setup-python with: python-version: '3.12' + groups: scripts - name: Create build directory shell: bash diff --git a/.github/actions/coverage/action.yml b/.github/actions/coverage/action.yml index 20ab363c377b..098a69572a97 100644 --- a/.github/actions/coverage/action.yml +++ b/.github/actions/coverage/action.yml @@ -42,41 +42,13 @@ runs: using: composite steps: - name: Generate Coverage Report - working-directory: ${{ inputs.build-dir }} shell: bash - env: - INPUT_MODE: ${{ inputs.mode }} run: | - case "${INPUT_MODE}" in - full) target="coverage" ;; - report-only) target="coverage-report" ;; - *) - echo "::error::Invalid coverage mode '${INPUT_MODE}'. Use 'full' or 'report-only'." - exit 1 - ;; - esac - - cmake --build . --target "${target}" 2>&1 | tee coverage-output.txt - - # Extract coverage summary for GitHub Step Summary - { - echo "## 📊 Code Coverage" - echo "" - echo "Mode: \`${INPUT_MODE}\`" - echo "" - if grep -q "lines:" coverage-output.txt; then - LINE_COV=$(grep "lines:" coverage-output.txt | tail -1) - BRANCH_COV=$(grep "branches:" coverage-output.txt | tail -1 || echo "branches: N/A") - echo "| Metric | Coverage |" - echo "|--------|----------|" - echo "| Lines | ${LINE_COV#*: } |" - echo "| Branches | ${BRANCH_COV#*: } |" - else - echo "Coverage data not available" - fi - echo "" - echo "[View detailed report](coverage.html)" - } >> "$GITHUB_STEP_SUMMARY" + python3 "${GITHUB_WORKSPACE}/tools/coverage.py" \ + --mode "${{ inputs.mode }}" \ + --build-dir "${{ inputs.build-dir }}" \ + --log-file "${{ inputs.build-dir }}/coverage-output.txt" \ + --step-summary - name: Upload to Codecov uses: codecov/codecov-action@v5 diff --git a/.github/actions/detect-changes/action.yml b/.github/actions/detect-changes/action.yml index 11a67d8994c1..06b035da4af9 100644 --- a/.github/actions/detect-changes/action.yml +++ b/.github/actions/detect-changes/action.yml @@ -3,7 +3,7 @@ description: Detect source, test, and CI changes for a platform inputs: platform: - description: "Platform (linux, windows, macos, android, ios)" + description: "Platform (linux, windows, macos, android, ios, docker-linux, docker-android)" required: true outputs: @@ -76,15 +76,23 @@ runs: echo "Changed files:" echo "${changed}" + # Map platform to its workflow file name + case "${platform}" in + docker-*) workflow_name="docker" ;; + *) workflow_name="${platform}" ;; + esac + relevant_patterns=( "^src/" "^CMakeLists.txt$" "^cmake/" "^libs/" + "^resources/" "^translations/" + "^.*\\.qrc$" "^deploy/${platform}/" "^test/" - "^\\.github/workflows/${platform}\\.yml$" + "^\\.github/workflows/${workflow_name}\\.yml$" "^\\.github/actions/" "^\\.github/scripts/" "^\\.github/build-config\\.json$" @@ -92,14 +100,22 @@ runs: case "${platform}" in android) relevant_patterns+=("^android/") ;; + docker-linux) + relevant_patterns+=("^deploy/docker/Dockerfile-build-ubuntu$" "^deploy/linux/") + ;; + docker-android) + relevant_patterns+=("^deploy/docker/Dockerfile-build-android$" "^android/" "^deploy/android/") + ;; esac case "${platform}" in - linux) relevant_patterns+=("^tools/setup/.*debian" "^tools/setup/install_dependencies\\.py$") ;; - windows) relevant_patterns+=("^tools/setup/.*windows") ;; - macos) relevant_patterns+=("^tools/setup/.*macos") ;; - android) relevant_patterns+=("^tools/setup/") ;; - ios) relevant_patterns+=("^tools/setup/.*ios" "^tools/setup/.*macos") ;; + linux) relevant_patterns+=("^tools/setup/.*debian" "^tools/setup/install_dependencies\\.py$") ;; + windows) relevant_patterns+=("^tools/setup/.*windows") ;; + macos) relevant_patterns+=("^tools/setup/.*macos") ;; + android) relevant_patterns+=("^tools/setup/") ;; + ios) relevant_patterns+=("^tools/setup/.*ios" "^tools/setup/.*macos") ;; + docker-linux) relevant_patterns+=("^tools/setup/") ;; + docker-android) relevant_patterns+=("^tools/setup/") ;; esac any_changed="false" diff --git a/.github/actions/download-all-artifacts/action.yml b/.github/actions/download-all-artifacts/action.yml index 9a45306d2141..dd120e31ac23 100644 --- a/.github/actions/download-all-artifacts/action.yml +++ b/.github/actions/download-all-artifacts/action.yml @@ -13,6 +13,10 @@ inputs: description: Comma-separated workflow names to download artifacts from required: false default: Linux,Windows,MacOS,Android + event: + description: Optional workflow event name to filter runs by + required: false + default: '' output-dir: description: Output directory for downloaded artifacts required: false @@ -43,10 +47,14 @@ runs: INPUT_HEAD_SHA: ${{ inputs.head-sha }} INPUT_OUTPUT_DIR: ${{ inputs.output-dir }} INPUT_WORKFLOWS: ${{ inputs.workflows }} + INPUT_EVENT: ${{ inputs.event }} INPUT_ARTIFACT_PREFIXES: ${{ inputs.artifact-prefixes }} INPUT_ARTIFACT_METADATA_FILE: ${{ inputs.artifact-metadata-file }} run: | ARGS=(--repo "$INPUT_REPO" --head-sha "$INPUT_HEAD_SHA" --output-dir "$INPUT_OUTPUT_DIR" --workflows "$INPUT_WORKFLOWS") + if [[ -n "$INPUT_EVENT" ]]; then + ARGS+=(--event "$INPUT_EVENT") + fi if [[ -n "$RUNS_FILE" && -f "$RUNS_FILE" ]]; then ARGS+=(--runs-file "$RUNS_FILE") fi diff --git a/.github/actions/gstreamer/action.yml b/.github/actions/gstreamer/action.yml index b5a13e44cc2b..3407645d261d 100644 --- a/.github/actions/gstreamer/action.yml +++ b/.github/actions/gstreamer/action.yml @@ -24,11 +24,14 @@ inputs: description: 'Upload to S3' required: false default: 'true' + aws-role-arn: + description: 'AWS IAM role ARN for OIDC authentication (preferred over static credentials)' + required: false aws-access-key-id: - description: 'AWS access key ID' + description: 'AWS access key ID (fallback if OIDC not available)' required: false aws-secret-access-key: - description: 'AWS secret access key' + description: 'AWS secret access key (fallback if OIDC not available)' required: false outputs: @@ -42,35 +45,7 @@ runs: - name: Read build config id: config shell: bash - env: - INPUT_VERSION: ${{ inputs.version }} - INPUT_PLATFORM: ${{ inputs.platform }} - INPUT_ARCH: ${{ inputs.arch }} - run: | - set -euo pipefail - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - echo "py_cmd=$py_cmd" >> "$GITHUB_OUTPUT" - - version="${INPUT_VERSION}" - if [[ -z "$version" ]]; then - case "${INPUT_PLATFORM}" in - macos) version_key="gstreamer_macos_version" ;; - windows) version_key="gstreamer_windows_version" ;; - android) version_key="gstreamer_android_version" ;; - *) version_key="gstreamer_version" ;; - esac - version=$("${py_cmd}" ./tools/setup/read_config.py --get "${version_key}") - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "qt_version=$("${py_cmd}" ./tools/setup/read_config.py --get qt_version)" >> "$GITHUB_OUTPUT" - echo "Building GStreamer $version for ${INPUT_PLATFORM} (${INPUT_ARCH})" + run: python3 ./tools/setup/resolve_gstreamer_config.py --platform "${{ inputs.platform }}" --version "${{ inputs.version }}" # ===== DEPENDENCIES ===== @@ -285,6 +260,21 @@ runs: # ===== ARCHIVE & UPLOAD ===== + - name: Configure AWS Credentials (OIDC) + if: inputs.upload-s3 == 'true' && inputs.aws-role-arn != '' + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ inputs.aws-role-arn }} + aws-region: us-west-2 + + - name: Configure AWS Credentials (Static) + if: inputs.upload-s3 == 'true' && inputs.aws-role-arn == '' && inputs.aws-access-key-id != '' + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-access-key-id: ${{ inputs.aws-access-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-region: us-west-2 + - name: Create archive id: archive shell: bash @@ -297,8 +287,6 @@ runs: SIMULATOR: ${{ inputs.simulator }} UPLOAD_S3: ${{ inputs.upload-s3 }} WORKSPACE: ${{ github.workspace }} - AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }} - AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }} AWS_DEFAULT_REGION: us-west-2 run: | set -euo pipefail @@ -309,7 +297,7 @@ runs: --output-dir "$OUTPUT_DIR" ) [[ "$SIMULATOR" == "true" ]] && args+=(--simulator) - [[ "$UPLOAD_S3" == "true" && -n "$AWS_ACCESS_KEY_ID" ]] && args+=(--upload-s3) + [[ "$UPLOAD_S3" == "true" ]] && args+=(--upload-s3) "${PY_CMD}" "${WORKSPACE}/.github/scripts/gstreamer_archive.py" "${args[@]}" diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index cb49199176a7..76f73025792b 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -114,20 +114,11 @@ runs: if: runner.os == 'Linux' shell: bash run: | - optional=$(python3 tools/setup/install_dependencies.py --platform debian --category gstreamer_optional --print-packages) + optional=$(python3 tools/setup/install_dependencies.py --platform debian --category gstreamer_optional --print-available-packages) if [ -n "$optional" ]; then - available=() - for pkg in $optional; do - if apt-cache show "$pkg" >/dev/null 2>&1; then - available+=("$pkg") - else - echo "Skipping unavailable optional package: $pkg" - fi - done - if [ ${#available[@]} -gt 0 ]; then - echo "Installing optional packages: ${available[*]}" - sudo apt-get -o DPkg::Lock::Timeout=300 install -y -qq "${available[@]}" || true - fi + echo "Installing optional packages: $optional" + # shellcheck disable=SC2086 + sudo apt-get -o DPkg::Lock::Timeout=300 install -y -qq $optional || true fi - name: Detect Python version for pipx cache (Linux) @@ -164,14 +155,9 @@ runs: timeout_minutes: 5 max_attempts: 3 command: | - for pkg in $EXTRA_PACKAGES; do - if [[ ! "$pkg" =~ ^[a-zA-Z0-9][a-zA-Z0-9.+-]*$ ]]; then - echo "Error: Invalid package name '$pkg'" >&2 - exit 1 - fi - done + validated="$(python3 tools/setup/install_dependencies.py --validate-extra-packages $EXTRA_PACKAGES)" # shellcheck disable=SC2086 - sudo apt-get install -y $EXTRA_PACKAGES + sudo apt-get install -y $validated - name: Install dependencies (macOS) if: runner.os == 'macOS' diff --git a/.github/actions/qt-install/action.yml b/.github/actions/qt-install/action.yml index c74b755ade4e..84ebea58f3e4 100644 --- a/.github/actions/qt-install/action.yml +++ b/.github/actions/qt-install/action.yml @@ -29,28 +29,6 @@ inputs: runs: using: composite steps: - - name: Compute Qt cache fingerprint - id: qt-cache-key - shell: bash - env: - QT_MODULES: ${{ inputs.modules }} - QT_ARCHIVES: ${{ inputs.archives }} - run: | - set -euo pipefail - cache_input="${QT_MODULES}"$'\n'"${QT_ARCHIVES}" - if command -v sha256sum >/dev/null 2>&1; then - digest="$(printf '%s' "$cache_input" | sha256sum | cut -d' ' -f1)" - else - digest="$(printf '%s' "$cache_input" | shasum -a 256 | awk '{print $1}')" - fi - echo "digest=${digest}" >> "$GITHUB_OUTPUT" - - - name: Restore extracted Qt cache - uses: actions/cache@v5 - with: - path: ${{ inputs.dir }}/Qt/${{ inputs.version }} - key: qt-extracted-${{ runner.os }}-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.arch }}-${{ inputs.version }}-${{ steps.qt-cache-key.outputs.digest }} - - name: Ensure pip available shell: bash run: python3 -m ensurepip 2>/dev/null || true diff --git a/.github/actions/setup-python/action.yml b/.github/actions/setup-python/action.yml index 9f65b41f9a35..51b5f8a2b15a 100644 --- a/.github/actions/setup-python/action.yml +++ b/.github/actions/setup-python/action.yml @@ -3,7 +3,7 @@ description: Setup Python with uv for fast dependency installation inputs: groups: - description: 'Dependency groups to install (comma-separated: precommit, test, ci, qt, coverage, dev, lsp, all)' + description: 'Dependency groups to install (comma-separated: scripts, precommit, test, ci, qt, coverage, dev, lsp, all)' required: false default: 'ci' python-version: @@ -24,6 +24,8 @@ runs: with: enable-cache: true cache-dependency-glob: | + tools/pyproject.toml + tools/uv.lock tools/setup/install_python.py .pre-commit-config.yaml @@ -32,15 +34,7 @@ runs: env: GROUPS: ${{ inputs.groups }} run: | - if command -v python3 >/dev/null 2>&1; then - py_cmd=python3 - elif command -v python >/dev/null 2>&1; then - py_cmd=python - else - echo "Error: Python not found in PATH" >&2 - exit 1 - fi - "$py_cmd" tools/setup/install_python.py "$GROUPS" + python3 tools/setup/install_python.py "$GROUPS" - name: Add venv to PATH shell: bash diff --git a/.github/actions/size-analysis/action.yml b/.github/actions/size-analysis/action.yml index bfa4d04b370a..aa4afd411a7a 100644 --- a/.github/actions/size-analysis/action.yml +++ b/.github/actions/size-analysis/action.yml @@ -40,12 +40,4 @@ runs: ) [[ "$INPUT_INSTALL_BLOATY" == "true" ]] && args+=(--install-bloaty) - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - "${py_cmd}" "${GITHUB_WORKSPACE}/.github/scripts/size_analysis.py" "${args[@]}" + python3 "${GITHUB_WORKSPACE}/.github/scripts/size_analysis.py" "${args[@]}" diff --git a/.github/actions/test-duration-report/action.yml b/.github/actions/test-duration-report/action.yml index 93f08b0578d6..eb30438d3881 100644 --- a/.github/actions/test-duration-report/action.yml +++ b/.github/actions/test-duration-report/action.yml @@ -48,160 +48,13 @@ runs: - name: Analyze test durations id: analyze shell: bash - env: - INPUT_JUNIT_PATH: ${{ inputs.junit-path }} - INPUT_BASELINE_PATH: ${{ inputs.baseline-path }} - INPUT_REPORT_JSON_PATH: ${{ inputs.report-json-path }} - INPUT_TOP_N: ${{ inputs.top-n }} - INPUT_SLOW_THRESHOLD_SECONDS: ${{ inputs.slow-threshold-seconds }} - INPUT_REGRESSION_FACTOR: ${{ inputs.regression-factor }} - INPUT_MIN_DELTA_SECONDS: ${{ inputs.min-delta-seconds }} - INPUT_FAIL_ON_REGRESSION: ${{ inputs.fail-on-regression }} run: | - python3 - <<'PY' - import json - import os - import sys - import xml.etree.ElementTree as ET - from pathlib import Path - - junit_path = Path(os.environ["INPUT_JUNIT_PATH"]) - baseline_path = Path(os.environ["INPUT_BASELINE_PATH"]) - report_json_path = Path(os.environ["INPUT_REPORT_JSON_PATH"]) - top_n = int(os.environ["INPUT_TOP_N"]) - slow_threshold = float(os.environ["INPUT_SLOW_THRESHOLD_SECONDS"]) - regression_factor = float(os.environ["INPUT_REGRESSION_FACTOR"]) - min_delta = float(os.environ["INPUT_MIN_DELTA_SECONDS"]) - fail_on_regression = os.environ["INPUT_FAIL_ON_REGRESSION"].lower() == "true" - - step_summary = Path(os.environ["GITHUB_STEP_SUMMARY"]) - github_output = Path(os.environ["GITHUB_OUTPUT"]) - - def test_key(elem: ET.Element) -> str: - classname = elem.attrib.get("classname", "").strip() - name = elem.attrib.get("name", "").strip() - if classname and name: - return f"{classname}::{name}" - return name or classname or "" - - def parse_time(value: str) -> float: - try: - return float(value) - except Exception: - return 0.0 - - if not junit_path.exists(): - msg = f"JUnit report not found at {junit_path}" - print(f"::warning::{msg}") - with step_summary.open("a", encoding="utf-8") as f: - f.write("## Test Duration Report\n\n") - f.write(f"- WARNING: {msg}\n") - with github_output.open("a", encoding="utf-8") as f: - f.write("regression_count=0\n") - f.write("slow_count=0\n") - sys.exit(0) - - tree = ET.parse(junit_path) - root = tree.getroot() - cases = [] - for testcase in root.iter("testcase"): - key = test_key(testcase) - secs = parse_time(testcase.attrib.get("time", "0")) - cases.append((key, secs)) - - total_seconds = sum(secs for _, secs in cases) - slowest = sorted(cases, key=lambda x: x[1], reverse=True) - slow_over_threshold = [(k, s) for k, s in slowest if s >= slow_threshold] - - baseline = {} - baseline_loaded = False - if baseline_path.exists(): - try: - payload = json.loads(baseline_path.read_text(encoding="utf-8")) - if isinstance(payload, dict) and "tests" in payload and isinstance(payload["tests"], dict): - raw = payload["tests"] - for key, value in raw.items(): - if isinstance(value, dict): - baseline[key] = float(value.get("seconds", 0.0)) - else: - baseline[key] = float(value) - baseline_loaded = True - elif isinstance(payload, dict): - for key, value in payload.items(): - if isinstance(value, (int, float)): - baseline[key] = float(value) - baseline_loaded = True - except Exception as e: - print(f"::warning::Failed to parse baseline file {baseline_path}: {e}") - - regressions = [] - if baseline: - for key, current in cases: - previous = baseline.get(key) - if previous is None or previous <= 0: - continue - if current >= previous * regression_factor and (current - previous) >= min_delta: - regressions.append((key, previous, current)) - - report = { - "junit_path": str(junit_path), - "total_tests": len(cases), - "total_seconds": total_seconds, - "slow_threshold_seconds": slow_threshold, - "slow_tests": [{"test": k, "seconds": s} for k, s in slow_over_threshold], - "regression_factor": regression_factor, - "min_delta_seconds": min_delta, - "regressions": [ - {"test": k, "baseline_seconds": b, "current_seconds": c, "delta_seconds": c - b} - for k, b, c in regressions - ], - } - report_json_path.parent.mkdir(parents=True, exist_ok=True) - report_json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8") - - with step_summary.open("a", encoding="utf-8") as f: - f.write("## Test Duration Report\n\n") - f.write(f"- JUnit: `{junit_path}`\n") - f.write(f"- Total tests: **{len(cases)}**\n") - f.write(f"- Total time: **{total_seconds:.1f}s**\n") - f.write(f"- Slow threshold: **{slow_threshold:.1f}s**\n") - if baseline_loaded: - f.write(f"- Baseline file: `{baseline_path}` ({len(baseline)} entries)\n") - else: - f.write(f"- Baseline file: `{baseline_path}` (not available)\n") - f.write("\n") - - if slowest: - f.write(f"### Top {min(top_n, len(slowest))} Slowest Tests\n\n") - f.write("| Test | Seconds |\n") - f.write("|---|---:|\n") - for key, secs in slowest[:top_n]: - f.write(f"| `{key}` | {secs:.3f} |\n") - f.write("\n") - - if regressions: - f.write("### Regressions vs Baseline\n\n") - f.write("| Test | Baseline (s) | Current (s) | Delta (s) |\n") - f.write("|---|---:|---:|---:|\n") - for key, base, cur in sorted(regressions, key=lambda x: x[2] - x[1], reverse=True)[:top_n]: - f.write(f"| `{key}` | {base:.3f} | {cur:.3f} | {cur - base:.3f} |\n") - f.write("\n") - else: - f.write("No baseline regressions detected.\n\n") - - for key, secs in slow_over_threshold[:top_n]: - print(f"::warning::Slow test (>={slow_threshold:.1f}s): {key} took {secs:.3f}s") - - for key, base, cur in sorted(regressions, key=lambda x: x[2] - x[1], reverse=True)[:top_n]: - print( - f"::warning::Timing regression: {key} baseline={base:.3f}s current={cur:.3f}s " - f"(+{(cur - base):.3f}s, x{(cur / base):.2f})" - ) - - with github_output.open("a", encoding="utf-8") as f: - f.write(f"regression_count={len(regressions)}\n") - f.write(f"slow_count={len(slow_over_threshold)}\n") - - if fail_on_regression and regressions: - sys.exit(1) - PY + python3 "${GITHUB_WORKSPACE}/.github/scripts/test_duration_report.py" \ + --junit-path "${{ inputs.junit-path }}" \ + --baseline-path "${{ inputs.baseline-path }}" \ + --report-json-path "${{ inputs.report-json-path }}" \ + --top-n "${{ inputs.top-n }}" \ + --slow-threshold-seconds "${{ inputs.slow-threshold-seconds }}" \ + --regression-factor "${{ inputs.regression-factor }}" \ + --min-delta-seconds "${{ inputs.min-delta-seconds }}" \ + ${{ inputs.fail-on-regression == 'true' && '--fail-on-regression' || '' }} diff --git a/.github/actions/verify-executable/action.yml b/.github/actions/verify-executable/action.yml index 3052bca4ddd2..dfeb553b82b1 100644 --- a/.github/actions/verify-executable/action.yml +++ b/.github/actions/verify-executable/action.yml @@ -61,15 +61,6 @@ runs: run_binary="${PWD}/${binary_name}" fi - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - set +e args=( --binary "${run_binary}" @@ -80,7 +71,7 @@ runs: if [[ "${run_headless}" == "true" ]]; then args+=(--headless) fi - "${py_cmd}" "${GITHUB_WORKSPACE}/tools/run_tests.py" "${args[@]}" + python3 "${GITHUB_WORKSPACE}/tools/run_tests.py" "${args[@]}" exit_code=$? set -e diff --git a/.github/build-config.json b/.github/build-config.json index ee7cbb63d72d..ea51b1b24266 100644 --- a/.github/build-config.json +++ b/.github/build-config.json @@ -1,5 +1,6 @@ { "android_build_tools": "35.0.0", + "ccache_version": "4.13.1", "android_cmdline_tools": "13114758", "android_min_sdk": "28", "android_platform": "35", diff --git a/.github/codecov.yml b/.github/codecov.yml index cef4e70c46ed..e817c6eb2cd3 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,8 +1,8 @@ codecov: - require_ci_to_pass: yes + require_ci_to_pass: true notify: - after_n_builds: 2 # Wait for at least 2 builds (Debug + Release) - wait_for_ci: yes + after_n_builds: 1 # Only Linux Debug generates coverage + wait_for_ci: true coverage: precision: 2 @@ -26,9 +26,9 @@ coverage: comment: layout: "reach,diff,flags,tree,footer" behavior: default - require_changes: no - require_base: no - require_head: yes + require_changes: false + require_base: false + require_head: true ignore: - "test/**/*" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4ed07cc136b6..3bb6fbda7703 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,10 @@ # Dependabot configuration # To merge dependabot PRs, comment "@dependabot merge" after CI passes # See: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/managing-pull-requests-for-dependency-updates +# +# Dependency management split: +# - Dependabot: GitHub Actions (this file) +# - Renovate: npm, git-submodules, pip, pre-commit (see renovate.json) version: 2 updates: @@ -17,4 +21,3 @@ updates: github-actions: patterns: - "*" - # npm dependencies managed by Renovate (see renovate.json) diff --git a/.github/renovate.json b/.github/renovate.json index 4c20d12f02dd..606c24bd13b1 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -14,6 +14,7 @@ "ignorePaths": [ ".github/workflows/**" ], + "description": "Manages npm, git-submodules, pip, pre-commit. GitHub Actions are handled by Dependabot (see dependabot.yml).", "enabledManagers": [ "npm", "git-submodules", diff --git a/.github/scripts/README.md b/.github/scripts/README.md index f68ec9e3d72a..1de04b1b0809 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -8,16 +8,19 @@ Python helper scripts used by workflows and composite actions in this repository |---|---| | `android_boot_test.py` | Android emulator boot smoke test | | `benchmark_runner.py` | Run benchmark binaries and summarize output | +| `ccache_helper.py` | Ccache CI helper: config output, binary install, build summary | | `check_baseline_ready.py` | Verify baseline-cache update readiness for platform workflows | +| `ci_bootstrap.py` | Bootstrap helper that makes `tools/common` imports work for CI scripts | | `collect_artifact_sizes.py` | Collect artifact sizes for latest successful platform workflow runs | | `collect_build_status.py` | Collect latest platform/pre-commit status for build-results comments | | `coverage_comment.py` | Build coverage report comments | | `find_binary.py` | Locate produced binaries/artifacts in build trees | | `generate_build_results_comment.py` | Generate consolidated PR build-results comment | | `gstreamer_archive.py` | Package GStreamer builds and optionally upload to S3 | -| `ccache_helper.py` | Ccache CI helper: config output, binary install, build summary | +| `plan_docker_builds.py` | Generate Docker workflow build matrices from changed files | +| `precommit_results.py` | Normalize pre-commit outputs into uploaded CI artifacts | | `size_analysis.py` | Analyze binary size changes | -| `workflow_runs.py` | Shared helpers for listing workflow runs via GitHub API | +| `test_duration_report.py` | Generate test-duration reports and regressions | ## Tests diff --git a/.github/scripts/ccache_helper.py b/.github/scripts/ccache_helper.py index 8ba2558dd9e8..d2c31733da80 100755 --- a/.github/scripts/ccache_helper.py +++ b/.github/scripts/ccache_helper.py @@ -27,11 +27,17 @@ import tarfile import tempfile import time +import zipfile from pathlib import Path from typing import NamedTuple from urllib.error import HTTPError, URLError from urllib.request import urlretrieve +WINDOWS_BINARY_SHA256 = { + "x86_64": "1c78a0b816a3174d4b170b96294e016a21fb4a577dfd8361e7322f77f85c6348", + "aarch64": "5907c8f74dcfff920ee57df55a5cb98164630665926b2ba9085e9f5fb701c58f", +} + # --------------------------------------------------------------------------- # Data classes @@ -468,6 +474,179 @@ def output_github_actions(config: CcacheConfig) -> None: print(f"Warning: Failed to write GitHub output: {e}", file=sys.stderr) +def write_github_outputs(values: dict[str, str]) -> None: + """Write arbitrary key/value pairs to ``$GITHUB_OUTPUT`` when available.""" + github_output = os.environ.get("GITHUB_OUTPUT") + if not github_output: + return + + try: + with open(github_output, "a") as f: + for key, value in values.items(): + f.write(f"{key}={value}\n") + except OSError as e: + print(f"Warning: Failed to write GitHub output: {e}", file=sys.stderr) + + +def append_github_env(values: dict[str, str]) -> None: + """Append environment variables to ``$GITHUB_ENV`` when available.""" + github_env = os.environ.get("GITHUB_ENV") + if not github_env: + return + with open(github_env, "a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def append_github_path(path_entry: str) -> None: + """Append a path entry to ``$GITHUB_PATH`` when available.""" + github_path = os.environ.get("GITHUB_PATH") + if not github_path: + return + with open(github_path, "a", encoding="utf-8") as handle: + handle.write(f"{path_entry}\n") + + +def determine_cache_scope(event_name: str, ref_name: str, pr_number: str = "") -> str: + """Return the normalized cache scope used by CI.""" + scope = "shared" + if event_name == "pull_request": + scope = f"pr-{pr_number or 'unknown'}" + elif event_name == "workflow_dispatch": + scope = f"manual-{ref_name}" + elif event_name == "push": + if ref_name != "master": + scope = f"branch-{ref_name}" + else: + scope = f"{event_name}-{ref_name}" + return scope.replace("/", "-") + + +def compute_cpm_fingerprint(root: Path) -> str: + """Hash CMake files that declare CPM or FetchContent dependencies.""" + declaration_re = re.compile(r"CPM(Add|Find)Package|FetchContent_Declare") + candidates: list[Path] = [] + for exact in [ + root / "CMakeLists.txt", + root / "cmake/modules/CPM.cmake", + root / ".github/build-config.json", + ]: + if exact.exists(): + candidates.append(exact) + + for directory in ("cmake",): + base = root / directory + if base.exists(): + candidates.extend(base.rglob("*.cmake")) + + for directory in ("src", "test"): + base = root / directory + if base.exists(): + candidates.extend(base.rglob("CMakeLists.txt")) + + dep_files: list[Path] = [] + for path in candidates: + if not path.is_file(): + continue + if path.name == "build-config.json": + dep_files.append(path) + continue + try: + text = path.read_text(encoding="utf-8", errors="ignore") + except OSError: + continue + if declaration_re.search(text): + dep_files.append(path) + + digest = hashlib.sha256() + rel_paths = sorted({path.relative_to(root).as_posix() for path in dep_files}) + for rel_path in rel_paths: + path = root / rel_path + try: + content = path.read_bytes() + except OSError: + continue + digest.update(rel_path.encode("utf-8")) + digest.update(b"\0") + digest.update(content) + digest.update(b"\0") + return digest.hexdigest() + + +def resolve_windows_binary_config(host: str, target: str) -> dict[str, str]: + """Return Windows ccache binary arch and checksum for the requested target.""" + arch = "aarch64" if host == "windows_arm64" else "x86_64" + if target != "android" and host != "windows_arm64": + arch = "x86_64" + return {"arch": arch, "sha256": WINDOWS_BINARY_SHA256[arch]} + + +def install_windows_binary(version: str, arch: str, sha256: str, runner_temp: Path) -> Path: + """Download and install the ccache Windows binary under ``runner_temp``.""" + ccache_url = ( + f"https://github.com/ccache/ccache/releases/download/v{version}/" + f"ccache-{version}-windows-{arch}.zip" + ) + install_dir = runner_temp / f"ccache-{version}-windows-{arch}" + install_dir.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + archive_path = temp_path / "ccache.zip" + urlretrieve(ccache_url, archive_path) + actual = hashlib.sha256(archive_path.read_bytes()).hexdigest() + if actual != sha256: + raise RuntimeError(f"SHA256 mismatch: {actual} != {sha256}") + + with zipfile.ZipFile(archive_path) as archive: + archive.extractall(temp_path) + + source = temp_path / f"ccache-{version}-windows-{arch}" / "ccache.exe" + if not source.exists(): + raise FileNotFoundError(f"ccache.exe not found in downloaded archive for {arch}") + shutil.copy2(source, install_dir / "ccache.exe") + return install_dir + + +def add_windows_binary_to_path(version: str, arch: str, runner_temp: Path) -> Path: + """Add the installed Windows ccache binary directory to PATH.""" + install_dir = runner_temp / f"ccache-{version}-windows-{arch}" + binary = install_dir / "ccache.exe" + if not binary.exists(): + raise FileNotFoundError(f"ccache.exe not found at {binary}") + append_github_path(str(install_dir)) + result = subprocess.run([str(binary), "--version"], capture_output=True, text=True, check=False) + if result.stdout: + print(result.stdout.strip()) + return install_dir + + +def configure_ccache_environment(workspace: Path) -> Path: + """Configure standard ccache environment variables for GitHub Actions.""" + workspace_path = Path(str(workspace).replace("\\", "/")) + ccache_dir = workspace_path / ".ccache" + ccache_dir.mkdir(parents=True, exist_ok=True) + append_github_env( + { + "CCACHE_DIR": ccache_dir.as_posix(), + "CCACHE_BASEDIR": workspace_path.as_posix(), + "CCACHE_CONFIGPATH": (workspace_path / "tools" / "configs" / "ccache.conf").as_posix(), + "CCACHE_MAXSIZE": "2G", + } + ) + return ccache_dir + + +def configure_cpm_cache(path_value: str) -> Path: + """Normalize and create the CPM cache path and export it.""" + cache_path = Path(path_value.replace("\\", "/")) + cache_path.mkdir(parents=True, exist_ok=True) + posix_path = cache_path.as_posix() + append_github_env({"CPM_SOURCE_CACHE": posix_path}) + write_github_outputs({"path": posix_path}) + return cache_path + + # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- @@ -548,6 +727,38 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: # -- summary ------------------------------------------------------- sub.add_parser("summary", help="Write cache hit/miss summary to GitHub Step Summary") + # -- scope --------------------------------------------------------- + scope = sub.add_parser("scope", help="Compute the workflow cache scope") + scope.add_argument("--event-name", required=True, help="GitHub event name") + scope.add_argument("--ref-name", required=True, help="Git ref name") + scope.add_argument("--pr-number", default="", help="Pull request number") + + # -- fingerprint --------------------------------------------------- + fingerprint = sub.add_parser("fingerprint", help="Compute CPM dependency fingerprint") + fingerprint.add_argument("--root", type=Path, default=Path("."), help="Repository root") + + # -- windows-config ------------------------------------------------ + windows_cfg = sub.add_parser("windows-config", help="Resolve Windows ccache binary metadata") + windows_cfg.add_argument("--host", required=True, help="Workflow host input") + windows_cfg.add_argument("--target", required=True, help="Workflow target input") + + install_win = sub.add_parser("install-windows", help="Download and install Windows ccache binary") + install_win.add_argument("--version", required=True) + install_win.add_argument("--arch", required=True, choices=["x86_64", "aarch64"]) + install_win.add_argument("--sha256", required=True) + install_win.add_argument("--runner-temp", type=Path, required=True) + + add_win = sub.add_parser("add-windows-path", help="Add Windows ccache install dir to PATH") + add_win.add_argument("--version", required=True) + add_win.add_argument("--arch", required=True, choices=["x86_64", "aarch64"]) + add_win.add_argument("--runner-temp", type=Path, required=True) + + env_cfg = sub.add_parser("configure-env", help="Configure ccache GitHub environment variables") + env_cfg.add_argument("--workspace", type=Path, required=True) + + cpm_cfg = sub.add_parser("configure-cpm-cache", help="Configure CPM source cache path") + cpm_cfg.add_argument("--path", required=True) + return parser.parse_args(argv) @@ -599,7 +810,50 @@ def main(argv: list[str] | None = None) -> int: if args.command == "summary": return run_summary() - print("Error: a subcommand is required (config, install, summary)", file=sys.stderr) + if args.command == "scope": + scope = determine_cache_scope(args.event_name, args.ref_name, args.pr_number) + print(scope) + write_github_outputs({"scope": scope}) + return 0 + + if args.command == "fingerprint": + fingerprint = compute_cpm_fingerprint(args.root.resolve()) + print(fingerprint) + write_github_outputs({"fingerprint": fingerprint}) + return 0 + + if args.command == "windows-config": + values = resolve_windows_binary_config(args.host, args.target) + print(f"arch={values['arch']}") + print(f"sha256={values['sha256']}") + write_github_outputs(values) + return 0 + + if args.command == "install-windows": + install_dir = install_windows_binary(args.version, args.arch, args.sha256, args.runner_temp) + print(install_dir) + write_github_outputs({"install_dir": str(install_dir)}) + return 0 + + if args.command == "add-windows-path": + install_dir = add_windows_binary_to_path(args.version, args.arch, args.runner_temp) + print(install_dir) + return 0 + + if args.command == "configure-env": + ccache_dir = configure_ccache_environment(args.workspace) + print(ccache_dir) + return 0 + + if args.command == "configure-cpm-cache": + cache_path = configure_cpm_cache(args.path) + print(cache_path) + return 0 + + print( + "Error: a subcommand is required (config, install, summary, scope, fingerprint, windows-config, install-windows, add-windows-path, configure-env, configure-cpm-cache)", + file=sys.stderr, + ) return 1 diff --git a/.github/scripts/check_baseline_ready.py b/.github/scripts/check_baseline_ready.py index 0db71e473e3f..287132478985 100644 --- a/.github/scripts/check_baseline_ready.py +++ b/.github/scripts/check_baseline_ready.py @@ -7,15 +7,18 @@ import hashlib import json import os -import sys -from pathlib import Path from typing import Any -_SCRIPT_DIR = Path(__file__).resolve().parent -if str(_SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(_SCRIPT_DIR)) +from ci_bootstrap import ensure_tools_dir -from workflow_runs import list_workflow_runs, parse_csv_list +ensure_tools_dir(__file__) + +from common.gh_actions import list_workflow_runs_for_sha + + +def parse_csv_list(value: str) -> list[str]: + """Parse comma-separated values into a trimmed non-empty list.""" + return [item.strip() for item in value.split(",") if item.strip()] def evaluate_readiness( @@ -94,7 +97,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: def main(argv: list[str] | None = None) -> int: args = parse_args(argv) platforms = parse_csv_list(args.platform_workflows) - runs = list_workflow_runs(args.repo, args.head_sha) + runs = list_workflow_runs_for_sha(args.repo, args.head_sha) if args.runs_cache: with open(args.runs_cache, "w", encoding="utf-8") as f: diff --git a/.github/scripts/ci_bootstrap.py b/.github/scripts/ci_bootstrap.py new file mode 100644 index 000000000000..a6e33b60138d --- /dev/null +++ b/.github/scripts/ci_bootstrap.py @@ -0,0 +1,16 @@ +"""Bootstrap helper for GitHub CI scripts that import from tools/.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def ensure_tools_dir(start: str | Path) -> Path: + """Ensure the repository's tools directory is importable.""" + path = Path(start).resolve() + repo_root = path.parents[2] + tools_dir = repo_root / "tools" + if str(tools_dir) not in sys.path: + sys.path.insert(0, str(tools_dir)) + return tools_dir diff --git a/.github/scripts/collect_artifact_sizes.py b/.github/scripts/collect_artifact_sizes.py index 64c1fbbce3f8..fd65b29b33bf 100644 --- a/.github/scripts/collect_artifact_sizes.py +++ b/.github/scripts/collect_artifact_sizes.py @@ -5,13 +5,22 @@ import argparse import concurrent.futures -from datetime import datetime import json -from pathlib import Path import sys +from pathlib import Path from typing import Any -from workflow_runs import list_run_artifacts, list_workflow_runs, parse_csv_list +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import list_run_artifacts, list_workflow_runs_for_sha +from common.github_runs import select_latest_runs_by_name + + +def parse_csv_list(value: str) -> list[str]: + """Parse comma-separated values into a trimmed non-empty list.""" + return [item.strip() for item in value.split(",") if item.strip()] _DISTRIBUTABLE_PREFIXES = ( @@ -39,48 +48,19 @@ def _is_distributable_artifact(name: str) -> bool: return False return any(name.startswith(prefix) for prefix in _DISTRIBUTABLE_PREFIXES) - -def _parse_created_at(created_at: Any) -> datetime | None: - value = str(created_at).strip() - if not value: - return None - if value.endswith("Z"): - value = f"{value[:-1]}+00:00" - try: - return datetime.fromisoformat(value) - except ValueError: - return None - - -def _is_newer_run(candidate: dict[str, Any], existing: dict[str, Any]) -> bool: - candidate_created_at = str(candidate.get("created_at", "")) - existing_created_at = str(existing.get("created_at", "")) - candidate_dt = _parse_created_at(candidate_created_at) - existing_dt = _parse_created_at(existing_created_at) - if candidate_dt is not None and existing_dt is not None: - return candidate_dt > existing_dt - return candidate_created_at > existing_created_at - - def latest_successful_runs( runs: list[dict[str, Any]], platforms: list[str], + *, + event: str = "", ) -> dict[str, dict[str, Any]]: - target = set(platforms) - latest: dict[str, dict[str, Any]] = {} - - for run in runs: - name = str(run.get("name", "")) - if name not in target: - continue - if str(run.get("status", "")) != "completed" or str(run.get("conclusion", "")) != "success": - continue - - existing = latest.get(name) - if existing is None or _is_newer_run(run, existing): - latest[name] = run - - return latest + return select_latest_runs_by_name( + runs, + set(platforms), + event=event, + status="completed", + conclusion="success", + ) def format_size_human(size_bytes: int) -> str: @@ -167,6 +147,12 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: default="Linux,Windows,MacOS,Android", help="Comma-separated platform workflow names", ) + parser.add_argument( + "--event", + default="", + choices=["", "push", "pull_request", "workflow_dispatch", "schedule"], + help="Optional workflow event name to filter runs by", + ) parser.add_argument( "--output-file", default="artifact-sizes.json", @@ -188,6 +174,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: def main(argv: list[str] | None = None) -> int: args = parse_args(argv) platforms = parse_csv_list(args.platform_workflows) + event = str(args.event).strip() if args.runs_file: try: @@ -204,7 +191,7 @@ def main(argv: list[str] | None = None) -> int: return 1 runs = loaded_runs else: - runs = list_workflow_runs(args.repo, args.head_sha) + runs = list_workflow_runs_for_sha(args.repo, args.head_sha) artifacts_by_run_id: dict[int, list[dict[str, Any]]] = {} if args.artifacts_file: artifacts_path = Path(args.artifacts_file) @@ -225,7 +212,7 @@ def main(argv: list[str] | None = None) -> int: artifact for artifact in run_artifacts if isinstance(artifact, dict) ] - latest = latest_successful_runs(runs, platforms) + latest = latest_successful_runs(runs, platforms, event=event) artifacts = collect_artifacts( args.repo, latest, diff --git a/.github/scripts/collect_build_status.py b/.github/scripts/collect_build_status.py index 76a65cfd559e..0918b4e5f78c 100644 --- a/.github/scripts/collect_build_status.py +++ b/.github/scripts/collect_build_status.py @@ -9,7 +9,17 @@ import os from typing import Any -from workflow_runs import list_workflow_runs, parse_csv_list +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import list_workflow_runs_for_sha +from common.github_runs import select_latest_runs_by_name + + +def parse_csv_list(value: str) -> list[str]: + """Parse comma-separated values into a trimmed non-empty list.""" + return [item.strip() for item in value.split(",") if item.strip()] def latest_runs_by_name( @@ -17,17 +27,7 @@ def latest_runs_by_name( names: set[str], event: str, ) -> dict[str, dict[str, Any]]: - latest: dict[str, dict[str, Any]] = {} - for run in runs: - name = str(run.get("name", "")) - if name not in names: - continue - if str(run.get("event", "")) != event: - continue - existing = latest.get(name) - if existing is None or str(run.get("created_at", "")) > str(existing.get("created_at", "")): - latest[name] = run - return latest + return select_latest_runs_by_name(runs, names, event=event) def platform_status( @@ -119,7 +119,7 @@ def main(argv: list[str] | None = None) -> int: target_names = set(platforms) target_names.add("pre-commit") - runs = list_workflow_runs(args.repo, args.head_sha) + runs = list_workflow_runs_for_sha(args.repo, args.head_sha) if args.runs_cache: with open(args.runs_cache, "w", encoding="utf-8") as f: diff --git a/.github/scripts/coverage_comment.py b/.github/scripts/coverage_comment.py index 4ee43e3aabdb..9c16e5430b88 100755 --- a/.github/scripts/coverage_comment.py +++ b/.github/scripts/coverage_comment.py @@ -6,24 +6,7 @@ import sys from pathlib import Path -try: - from defusedxml.ElementTree import ParseError as XMLParseError - from defusedxml.ElementTree import parse as _xml_parse_impl - _USING_DEFUSEDXML = True -except ImportError: - from xml.etree.ElementTree import ParseError as XMLParseError - from xml.etree.ElementTree import parse as _xml_parse_impl - _USING_DEFUSEDXML = False - - -def xml_parse(path: str): - if _USING_DEFUSEDXML: - return _xml_parse_impl(path) - # Harden stdlib fallback: reject XML with DTD/entities. - text = Path(path).read_text(encoding="utf-8", errors="replace") - if " dict | None: diff --git a/.github/scripts/find_binary.py b/.github/scripts/find_binary.py index 5ed8ab5cfd26..3799f90faadf 100644 --- a/.github/scripts/find_binary.py +++ b/.github/scripts/find_binary.py @@ -135,7 +135,7 @@ def output_github_actions(info: BinaryInfo) -> None: """Write outputs for GitHub Actions.""" github_output = os.environ.get("GITHUB_OUTPUT") if github_output: - with open(github_output, "a") as f: + with open(github_output, "a", encoding="utf-8") as f: f.write(f"binary_path={info.path}\n") f.write(f"binary_name={info.name}\n") f.write(f"binary_dir={info.directory}\n") diff --git a/.github/scripts/generate_build_results_comment.py b/.github/scripts/generate_build_results_comment.py index 1dc9c8febc99..a8a5106b8978 100644 --- a/.github/scripts/generate_build_results_comment.py +++ b/.github/scripts/generate_build_results_comment.py @@ -13,28 +13,11 @@ from typing import Mapping from urllib.parse import quote, urlsplit, urlunsplit -try: - from defusedxml.ElementTree import ParseError as XMLParseError - from defusedxml.ElementTree import parse as _xml_parse_impl - _USING_DEFUSEDXML = True -except ImportError: - from xml.etree.ElementTree import ParseError as XMLParseError - from xml.etree.ElementTree import parse as _xml_parse_impl - _USING_DEFUSEDXML = False +from xml_utils import XMLParseError, xml_parse as _xml_parse logger = logging.getLogger(__name__) -def _xml_parse(path: Path): - if _USING_DEFUSEDXML: - return _xml_parse_impl(path) - # Harden stdlib fallback: reject XML with DTD/entities. - text = path.read_text(encoding="utf-8", errors="replace") - if " str: return str(env.get(key, default)).strip() diff --git a/.github/scripts/plan_docker_builds.py b/.github/scripts/plan_docker_builds.py new file mode 100644 index 000000000000..d2f428f5fa85 --- /dev/null +++ b/.github/scripts/plan_docker_builds.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Plan Docker build matrix entries for the Docker workflow.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path + + +def plan_builds(event_name: str, linux_changed: bool, android_changed: bool) -> dict[str, object]: + """Return workflow matrix and a has_jobs flag.""" + include: list[dict[str, object]] = [] + + linux_selected = event_name != "pull_request" or linux_changed + android_selected = event_name != "pull_request" or android_changed + + if linux_selected: + include.append( + { + "platform": "Linux", + "dockerfile": "Dockerfile-build-ubuntu", + "fuse": True, + "artifact_pattern": "*.AppImage", + } + ) + if android_selected: + include.append( + { + "platform": "Android", + "dockerfile": "Dockerfile-build-android", + "fuse": False, + "artifact_pattern": "*.apk", + } + ) + + return {"matrix": {"include": include}, "has_jobs": bool(include)} + + +def write_github_output(path: Path, values: dict[str, str]) -> None: + """Append key/value outputs for GitHub Actions.""" + with path.open("a", encoding="utf-8") as handle: + for key, value in values.items(): + handle.write(f"{key}={value}\n") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Plan Docker workflow builds.") + parser.add_argument("--event-name", default=os.environ.get("EVENT_NAME", "")) + parser.add_argument("--linux", default=os.environ.get("LINUX", "false")) + parser.add_argument("--android", default=os.environ.get("ANDROID", "false")) + parser.add_argument("--github-output", default=os.environ.get("GITHUB_OUTPUT", "")) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Compute the Docker build matrix and emit outputs.""" + args = parse_args(argv) + plan = plan_builds( + args.event_name, + args.linux == "true", + args.android == "true", + ) + matrix_json = json.dumps(plan["matrix"], separators=(",", ":")) + print(matrix_json) + + if args.github_output: + write_github_output( + Path(args.github_output), + { + "matrix": matrix_json, + "has_jobs": "true" if plan["has_jobs"] else "false", + }, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/precommit_results.py b/.github/scripts/precommit_results.py new file mode 100644 index 000000000000..d75014b3d365 --- /dev/null +++ b/.github/scripts/precommit_results.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Prepare pre-commit result artifacts for CI.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import sys +from pathlib import Path + + +def write_results_json( + output_dir: Path, + *, + exit_code: str, + passed: str, + failed: str, + skipped: str, + run_url: str, +) -> Path: + """Write the summary JSON artifact.""" + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / "pre-commit-results.json" + output_path.write_text( + json.dumps( + { + "exit_code": exit_code, + "passed": passed, + "failed": failed, + "skipped": skipped, + "run_url": run_url, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + return output_path + + +def copy_output_file(source: Path | None, output_dir: Path) -> None: + """Copy the text output file into the artifact directory if it exists.""" + if source is None or not source.exists(): + return + shutil.copy2(source, output_dir / source.name) + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Prepare pre-commit result artifacts.") + parser.add_argument("--output-dir", default="pre-commit-results") + parser.add_argument("--exit-code", default="1") + parser.add_argument("--passed", default="0") + parser.add_argument("--failed", default="0") + parser.add_argument("--skipped", default="0") + parser.add_argument("--run-url", default="") + parser.add_argument("--output-file", default="") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Generate the artifact directory for pre-commit results.""" + args = parse_args(argv) + output_dir = Path(args.output_dir) + write_results_json( + output_dir, + exit_code=args.exit_code, + passed=args.passed, + failed=args.failed, + skipped=args.skipped, + run_url=args.run_url, + ) + copy_output_file(Path(args.output_file) if args.output_file else None, output_dir) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/test_duration_report.py b/.github/scripts/test_duration_report.py new file mode 100644 index 000000000000..5ea44097ff5f --- /dev/null +++ b/.github/scripts/test_duration_report.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +"""Analyze JUnit test durations and emit CI summaries.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any + + +def test_key(elem: ET.Element) -> str: + """Return a stable identifier for a JUnit testcase element.""" + classname = elem.attrib.get("classname", "").strip() + name = elem.attrib.get("name", "").strip() + if classname and name: + return f"{classname}::{name}" + return name or classname or "" + + +def parse_time(value: str) -> float: + """Parse testcase duration, defaulting invalid values to 0.""" + try: + return float(value) + except Exception: + return 0.0 + + +def load_baseline(baseline_path: Path) -> tuple[dict[str, float], bool, str]: + """Load a baseline file into a simple test->seconds mapping.""" + if not baseline_path.exists(): + return {}, False, "" + + try: + payload = json.loads(baseline_path.read_text(encoding="utf-8")) + baseline: dict[str, float] = {} + if isinstance(payload, dict) and "tests" in payload and isinstance(payload["tests"], dict): + for key, value in payload["tests"].items(): + if isinstance(value, dict): + baseline[key] = float(value.get("seconds", 0.0)) + else: + baseline[key] = float(value) + return baseline, True, "" + if isinstance(payload, dict): + for key, value in payload.items(): + if isinstance(value, (int, float)): + baseline[key] = float(value) + return baseline, True, "" + return {}, False, "baseline file has unsupported structure" + except Exception as exc: + return {}, False, str(exc) + + +def analyze_test_durations( + junit_path: Path, + baseline_path: Path, + *, + top_n: int, + slow_threshold: float, + regression_factor: float, + min_delta: float, +) -> dict[str, Any]: + """Return computed timing report data for a JUnit XML file.""" + tree = ET.parse(junit_path) + root = tree.getroot() + cases = [(test_key(testcase), parse_time(testcase.attrib.get("time", "0"))) for testcase in root.iter("testcase")] + + total_seconds = sum(secs for _, secs in cases) + slowest = sorted(cases, key=lambda item: item[1], reverse=True) + slow_over_threshold = [(key, secs) for key, secs in slowest if secs >= slow_threshold] + + baseline, baseline_loaded, baseline_error = load_baseline(baseline_path) + regressions: list[tuple[str, float, float]] = [] + if baseline: + for key, current in cases: + previous = baseline.get(key) + if previous is None or previous <= 0: + continue + if current >= previous * regression_factor and (current - previous) >= min_delta: + regressions.append((key, previous, current)) + + regressions.sort(key=lambda item: item[2] - item[1], reverse=True) + + return { + "junit_path": str(junit_path), + "total_tests": len(cases), + "total_seconds": total_seconds, + "slow_threshold_seconds": slow_threshold, + "slow_tests": [{"test": key, "seconds": secs} for key, secs in slow_over_threshold], + "regression_factor": regression_factor, + "min_delta_seconds": min_delta, + "regressions": [ + { + "test": key, + "baseline_seconds": baseline_secs, + "current_seconds": current_secs, + "delta_seconds": current_secs - baseline_secs, + } + for key, baseline_secs, current_secs in regressions + ], + "baseline_loaded": baseline_loaded, + "baseline_entries": len(baseline), + "baseline_error": baseline_error, + "top_slowest": [{"test": key, "seconds": secs} for key, secs in slowest[:top_n]], + "regression_count": len(regressions), + "slow_count": len(slow_over_threshold), + "regression_warnings": regressions[:top_n], + "slow_warnings": slow_over_threshold[:top_n], + } + + +def build_summary(report: dict[str, Any], baseline_path: Path, *, missing_junit: str = "") -> str: + """Render a markdown summary for GitHub Step Summary.""" + lines = ["## Test Duration Report", ""] + if missing_junit: + lines.append(f"- WARNING: {missing_junit}") + lines.append("") + return "\n".join(lines) + + lines.extend( + [ + f"- JUnit: `{report['junit_path']}`", + f"- Total tests: **{report['total_tests']}**", + f"- Total time: **{report['total_seconds']:.1f}s**", + f"- Slow threshold: **{report['slow_threshold_seconds']:.1f}s**", + ] + ) + if report["baseline_loaded"]: + lines.append(f"- Baseline file: `{baseline_path}` ({report['baseline_entries']} entries)") + else: + suffix = f" - {report['baseline_error']}" if report["baseline_error"] else "" + lines.append(f"- Baseline file: `{baseline_path}` (not available{suffix})") + lines.append("") + + top_slowest = report["top_slowest"] + if top_slowest: + lines.append(f"### Top {len(top_slowest)} Slowest Tests") + lines.append("") + lines.append("| Test | Seconds |") + lines.append("|---|---:|") + for item in top_slowest: + lines.append(f"| `{item['test']}` | {item['seconds']:.3f} |") + lines.append("") + + regressions = report["regressions"] + if regressions: + lines.append("### Regressions vs Baseline") + lines.append("") + lines.append("| Test | Baseline (s) | Current (s) | Delta (s) |") + lines.append("|---|---:|---:|---:|") + for item in regressions[: len(top_slowest) or len(regressions)]: + lines.append( + f"| `{item['test']}` | {item['baseline_seconds']:.3f} | {item['current_seconds']:.3f} | {item['delta_seconds']:.3f} |" + ) + lines.append("") + else: + lines.append("No baseline regressions detected.") + lines.append("") + + return "\n".join(lines) + + +def append_file(path: Path | None, content: str) -> None: + """Append content to a file if a path is provided.""" + if path is None: + return + with path.open("a", encoding="utf-8") as handle: + handle.write(content) + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Analyze JUnit test durations.") + parser.add_argument("--junit-path", required=True) + parser.add_argument("--baseline-path", default=".github/test-duration-baseline.json") + parser.add_argument("--report-json-path", default="test-duration-report.json") + parser.add_argument("--top-n", type=int, default=20) + parser.add_argument("--slow-threshold-seconds", type=float, default=60.0) + parser.add_argument("--regression-factor", type=float, default=1.5) + parser.add_argument("--min-delta-seconds", type=float, default=5.0) + parser.add_argument("--fail-on-regression", action="store_true") + parser.add_argument("--github-step-summary", default=os.environ.get("GITHUB_STEP_SUMMARY", "")) + parser.add_argument("--github-output", default=os.environ.get("GITHUB_OUTPUT", "")) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Run the report generation flow.""" + args = parse_args(argv) + junit_path = Path(args.junit_path) + baseline_path = Path(args.baseline_path) + report_json_path = Path(args.report_json_path) + step_summary = Path(args.github_step_summary) if args.github_step_summary else None + github_output = Path(args.github_output) if args.github_output else None + + if not junit_path.exists(): + message = f"JUnit report not found at {junit_path}" + print(f"::warning::{message}") + append_file(step_summary, build_summary({}, baseline_path, missing_junit=message)) + append_file(github_output, "regression_count=0\nslow_count=0\n") + return 0 + + report = analyze_test_durations( + junit_path, + baseline_path, + top_n=args.top_n, + slow_threshold=args.slow_threshold_seconds, + regression_factor=args.regression_factor, + min_delta=args.min_delta_seconds, + ) + report_json_path.parent.mkdir(parents=True, exist_ok=True) + report_json_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + append_file(step_summary, build_summary(report, baseline_path)) + append_file( + github_output, + f"regression_count={report['regression_count']}\nslow_count={report['slow_count']}\n", + ) + + if report["baseline_error"]: + print(f"::warning::Failed to parse baseline file {baseline_path}: {report['baseline_error']}") + + for key, secs in report["slow_warnings"]: + print(f"::warning::Slow test (>={args.slow_threshold_seconds:.1f}s): {key} took {secs:.3f}s") + for key, base, cur in report["regression_warnings"]: + print( + f"::warning::Timing regression: {key} baseline={base:.3f}s current={cur:.3f}s " + f"(+{(cur - base):.3f}s, x{(cur / base):.2f})" + ) + + return 1 if args.fail_on_regression and report["regression_count"] else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/tests/test_ccache_helper.py b/.github/scripts/tests/test_ccache_helper.py index 9a2b8bc9fbfd..de2c1b7ac7e2 100644 --- a/.github/scripts/tests/test_ccache_helper.py +++ b/.github/scripts/tests/test_ccache_helper.py @@ -13,8 +13,13 @@ CcacheConfig, CcacheInstaller, build_summary_markdown, + configure_ccache_environment, + configure_cpm_cache, + compute_cpm_fingerprint, + determine_cache_scope, main, resolve_arch, + resolve_windows_binary_config, parse_args, write_step_summary, ) @@ -242,3 +247,66 @@ def test_summary_with_stats(self, tmp_path): assert "12 / 17" in content assert "
    " in content assert "cache hit (direct): 10" in content + + +class TestCacheScope: + """Tests for workflow cache scope helper.""" + + def test_pull_request_scope(self): + assert determine_cache_scope("pull_request", "feature/test", "42") == "pr-42" + + def test_push_non_master_scope(self): + assert determine_cache_scope("push", "feature/test") == "branch-feature-test" + + +class TestFingerprint: + """Tests for CPM fingerprint helper.""" + + def test_fingerprint_changes_when_dependency_file_changes(self, tmp_path): + (tmp_path / "cmake/modules").mkdir(parents=True) + (tmp_path / ".github").mkdir() + (tmp_path / "CMakeLists.txt").write_text("project(QGC)\nCPMAddPackage(NAME foo)\n") + (tmp_path / "cmake/modules/CPM.cmake").write_text("# helper\n") + (tmp_path / ".github/build-config.json").write_text("{}\n") + + before = compute_cpm_fingerprint(tmp_path) + (tmp_path / "CMakeLists.txt").write_text("project(QGC)\nCPMAddPackage(NAME bar)\n") + after = compute_cpm_fingerprint(tmp_path) + + assert before != after + + +class TestWindowsConfig: + """Tests for Windows ccache binary resolution.""" + + def test_windows_arm64_uses_aarch64_binary(self): + values = resolve_windows_binary_config("windows_arm64", "windows") + assert values["arch"] == "aarch64" + + def test_android_windows_uses_x86_64_binary(self): + values = resolve_windows_binary_config("windows", "android") + assert values["arch"] == "x86_64" + + +class TestEnvironmentConfig: + """Tests for environment configuration helpers.""" + + def test_configure_ccache_environment_writes_env(self, tmp_path): + github_env = tmp_path / "env.txt" + workspace = tmp_path / "workspace" + with patch.dict(os.environ, {"GITHUB_ENV": str(github_env)}): + ccache_dir = configure_ccache_environment(workspace) + assert ccache_dir == workspace / ".ccache" + content = github_env.read_text() + assert "CCACHE_DIR=" in content + assert "CCACHE_CONFIGPATH=" in content + + def test_configure_cpm_cache_writes_outputs(self, tmp_path): + github_env = tmp_path / "env.txt" + github_output = tmp_path / "output.txt" + cache = tmp_path / "cpm-cache" + with patch.dict(os.environ, {"GITHUB_ENV": str(github_env), "GITHUB_OUTPUT": str(github_output)}): + configured = configure_cpm_cache(str(cache)) + assert configured == cache + assert "CPM_SOURCE_CACHE=" in github_env.read_text() + assert "path=" in github_output.read_text() diff --git a/.github/scripts/tests/test_collect_artifact_sizes.py b/.github/scripts/tests/test_collect_artifact_sizes.py index 25be628cd545..bd2f43e14845 100644 --- a/.github/scripts/tests/test_collect_artifact_sizes.py +++ b/.github/scripts/tests/test_collect_artifact_sizes.py @@ -15,6 +15,7 @@ def _run( created_at: str = "2026-02-24T00:00:00Z", status: str = "completed", conclusion: str = "success", + event: str = "pull_request", ) -> dict[str, object]: return { "id": run_id, @@ -22,6 +23,7 @@ def _run( "created_at": created_at, "status": status, "conclusion": conclusion, + "event": event, } @@ -51,6 +53,17 @@ def test_latest_successful_runs_handles_iso8601_offsets() -> None: assert latest["Linux"]["id"] == 2 +def test_latest_successful_runs_filters_by_event() -> None: + platforms = ["Linux"] + runs = [ + _run("Linux", run_id=1, created_at="2026-02-24T00:00:00Z", event="pull_request"), + _run("Linux", run_id=2, created_at="2026-02-24T01:00:00Z", event="push"), + ] + + latest = mod.latest_successful_runs(runs, platforms, event="pull_request") + assert latest["Linux"]["id"] == 1 + + def test_collect_artifacts_filters_non_product_artifacts() -> None: latest = {"Linux": {"id": 11}} @@ -210,7 +223,7 @@ def fake_list_run_artifacts(repo: str, run_id: int) -> list[dict[str, object]]: return [{"name": "QGroundControl-x86_64", "size_in_bytes": 2 * 1024 * 1024}] return [{"name": "QGroundControl-installer-AMD64", "size_in_bytes": 2 * 1024 * 1024}] - monkeypatch.setattr(mod, "list_workflow_runs", fake_list_workflow_runs) + monkeypatch.setattr(mod, "list_workflow_runs_for_sha", fake_list_workflow_runs) monkeypatch.setattr(mod, "list_run_artifacts", fake_list_run_artifacts) rc = mod.main( @@ -258,7 +271,7 @@ def fake_list_workflow_runs(repo: str, head_sha: str) -> list[dict[str, object]] def fail_list_run_artifacts(repo: str, run_id: int) -> list[dict[str, object]]: raise AssertionError("list_run_artifacts should not be called when artifacts file is provided") - monkeypatch.setattr(mod, "list_workflow_runs", fake_list_workflow_runs) + monkeypatch.setattr(mod, "list_workflow_runs_for_sha", fake_list_workflow_runs) monkeypatch.setattr(mod, "list_run_artifacts", fail_list_run_artifacts) rc = mod.main( diff --git a/.github/scripts/tests/test_collect_build_status.py b/.github/scripts/tests/test_collect_build_status.py index 5e3fb26de5fc..a45f054af055 100644 --- a/.github/scripts/tests/test_collect_build_status.py +++ b/.github/scripts/tests/test_collect_build_status.py @@ -55,7 +55,7 @@ def test_main_writes_expected_outputs(tmp_path: Path, monkeypatch: pytest.Monkey _run("Android", html_url="https://example.test/android"), _run("pre-commit", run_id=99, html_url="https://example.test/precommit"), ] - monkeypatch.setattr(mod, "list_workflow_runs", lambda repo, sha: runs) + monkeypatch.setattr(mod, "list_workflow_runs_for_sha", lambda repo, sha: runs) rc = mod.main( [ diff --git a/.github/scripts/tests/test_plan_docker_builds.py b/.github/scripts/tests/test_plan_docker_builds.py new file mode 100644 index 000000000000..7331e1f68a63 --- /dev/null +++ b/.github/scripts/tests/test_plan_docker_builds.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from plan_docker_builds import plan_builds + + +def test_plan_builds_pull_request_filters_by_changes(): + plan = plan_builds("pull_request", linux_changed=True, android_changed=False) + assert plan["has_jobs"] is True + assert plan["matrix"]["include"] == [ + { + "platform": "Linux", + "dockerfile": "Dockerfile-build-ubuntu", + "fuse": True, + "artifact_pattern": "*.AppImage", + } + ] + + +def test_plan_builds_push_includes_all(): + plan = plan_builds("push", linux_changed=False, android_changed=False) + assert len(plan["matrix"]["include"]) == 2 diff --git a/.github/scripts/tests/test_precommit_results.py b/.github/scripts/tests/test_precommit_results.py new file mode 100644 index 000000000000..a35b78215b27 --- /dev/null +++ b/.github/scripts/tests/test_precommit_results.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from pathlib import Path + +from precommit_results import main + + +def test_precommit_results_writes_json_and_copies_output(tmp_path): + output_file = tmp_path / "pre-commit-output.txt" + output_file.write_text("hook output\n", encoding="utf-8") + out_dir = tmp_path / "artifact" + + result = main( + [ + "--output-dir", + str(out_dir), + "--exit-code", + "1", + "--passed", + "10", + "--failed", + "2", + "--skipped", + "1", + "--run-url", + "https://example.test/run/1", + "--output-file", + str(output_file), + ] + ) + + assert result == 0 + assert (out_dir / "pre-commit-results.json").exists() + assert (out_dir / "pre-commit-output.txt").read_text(encoding="utf-8") == "hook output\n" diff --git a/.github/scripts/tests/test_test_duration_report.py b/.github/scripts/tests/test_test_duration_report.py new file mode 100644 index 000000000000..b2b95bc16b23 --- /dev/null +++ b/.github/scripts/tests/test_test_duration_report.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import json + +from test_duration_report import analyze_test_durations, main + + +def _write_junit(path, xml: str) -> None: + path.write_text(xml, encoding="utf-8") + + +def test_analyze_test_durations_detects_regression(tmp_path): + junit = tmp_path / "junit.xml" + baseline = tmp_path / "baseline.json" + _write_junit( + junit, + """ + + + """, + ) + baseline.write_text(json.dumps({"tests": {"Suite::slow": {"seconds": 2.0}}}), encoding="utf-8") + + report = analyze_test_durations( + junit, + baseline, + top_n=5, + slow_threshold=5.0, + regression_factor=1.5, + min_delta=5.0, + ) + + assert report["slow_count"] == 1 + assert report["regression_count"] == 1 + + +def test_test_duration_report_main_missing_junit(tmp_path): + summary = tmp_path / "summary.md" + output = tmp_path / "output.txt" + result = main( + [ + "--junit-path", + str(tmp_path / "missing.xml"), + "--github-step-summary", + str(summary), + "--github-output", + str(output), + ] + ) + assert result == 0 + assert "regression_count=0" in output.read_text(encoding="utf-8") + assert "WARNING" in summary.read_text(encoding="utf-8") diff --git a/.github/scripts/tests/test_workflow_runs.py b/.github/scripts/tests/test_workflow_runs.py deleted file mode 100644 index 460fe9bbdb5c..000000000000 --- a/.github/scripts/tests/test_workflow_runs.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Tests for workflow_runs.py.""" - -from __future__ import annotations - -import json - -import workflow_runs as mod - - -def test_parse_json_documents_handles_paginated_stream() -> None: - payload = ( - json.dumps({"workflow_runs": [{"id": 1, "name": "Linux"}]}) - + "\n" - + json.dumps({"workflow_runs": [{"id": 2, "name": "Windows"}]}) - ) - - docs = mod.parse_json_documents(payload) - assert len(docs) == 2 - assert docs[0]["workflow_runs"][0]["id"] == 1 - assert docs[1]["workflow_runs"][0]["id"] == 2 - - -def test_list_workflow_runs_delegates_to_shared_helper(monkeypatch) -> None: - def fake_list(repo: str, head_sha: str) -> list[dict[str, object]]: - assert repo == "owner/repo" - assert head_sha == "abc123" - return [{"id": 1, "name": "Linux"}] - - monkeypatch.setattr(mod, "_list_workflow_runs_for_sha", fake_list) - runs = mod.list_workflow_runs("owner/repo", "abc123") - assert runs == [{"id": 1, "name": "Linux"}] diff --git a/.github/scripts/workflow_runs.py b/.github/scripts/workflow_runs.py deleted file mode 100644 index 93efc8b64064..000000000000 --- a/.github/scripts/workflow_runs.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -"""Helpers for querying GitHub Actions workflow runs via gh CLI.""" - -from __future__ import annotations - -import sys -from pathlib import Path -from typing import Any - -# Ensure repository root is importable when scripts are executed by path. -_REPO_ROOT = Path(__file__).resolve().parents[2] -_COMMON_DIR = _REPO_ROOT / "tools" / "common" -if str(_COMMON_DIR) not in sys.path: - sys.path.insert(0, str(_COMMON_DIR)) - -import gh_actions as _gh_actions - -_gh = _gh_actions.gh -parse_json_documents = _gh_actions.parse_json_documents -list_run_artifacts = _gh_actions.list_run_artifacts -_list_workflow_runs_for_sha = _gh_actions.list_workflow_runs_for_sha - - -def parse_csv_list(value: str) -> list[str]: - """Parse comma-separated values into a trimmed non-empty list.""" - return [item.strip() for item in value.split(",") if item.strip()] - - -def list_workflow_runs(repo: str, head_sha: str) -> list[dict[str, Any]]: - """List workflow runs for a commit SHA across all pages.""" - return _list_workflow_runs_for_sha(repo, head_sha) diff --git a/.github/scripts/xml_utils.py b/.github/scripts/xml_utils.py new file mode 100644 index 000000000000..125d03dff2c1 --- /dev/null +++ b/.github/scripts/xml_utils.py @@ -0,0 +1,27 @@ +"""Safe XML parsing with defusedxml fallback.""" + +from pathlib import Path + +try: + from defusedxml.ElementTree import ParseError as XMLParseError + from defusedxml.ElementTree import parse as _xml_parse_impl + + _USING_DEFUSEDXML = True +except ImportError: + from xml.etree.ElementTree import ParseError as XMLParseError + from xml.etree.ElementTree import parse as _xml_parse_impl + + _USING_DEFUSEDXML = False + + +def xml_parse(path): + """Parse XML safely, rejecting DTD/ENTITY declarations when defusedxml is unavailable.""" + if _USING_DEFUSEDXML: + return _xml_parse_impl(path) + text = Path(path).read_text(encoding="utf-8", errors="replace") + if "- - github.repository_owner == 'mavlink' && - ( - github.event_name == 'push' || - (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'upload') - ) + sync: + name: Crowdin Sync + if: github.repository_owner == 'mavlink' runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read steps: + - name: Determine action + id: action + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_ACTION: ${{ inputs.action }} + PR_MERGED: ${{ github.event.pull_request.merged }} + run: | + if [[ "$EVENT_NAME" == "schedule" ]] || [[ "$EVENT_NAME" == "workflow_dispatch" && "$INPUT_ACTION" == "download" ]]; then + echo "mode=download" >> "$GITHUB_OUTPUT" + elif [[ "$EVENT_NAME" == "push" ]] || [[ "$EVENT_NAME" == "pull_request" && "$PR_MERGED" == "true" ]] || [[ "$EVENT_NAME" == "workflow_dispatch" && "$INPUT_ACTION" == "upload" ]]; then + echo "mode=upload" >> "$GITHUB_OUTPUT" + else + echo "mode=skip" >> "$GITHUB_OUTPUT" + fi + - name: Harden Runner + if: steps.action.outputs.mode != 'skip' uses: step-security/harden-runner@v2 with: egress-policy: audit - name: Checkout + if: steps.action.outputs.mode != 'skip' uses: actions/checkout@v6 - name: Upload sources to Crowdin + if: steps.action.outputs.mode == 'upload' uses: crowdin/github-action@v2 with: config: 'docs/crowdin_docs.yml' @@ -64,30 +76,8 @@ jobs: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_DOCS_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.PX4BUILDBOT_CROWDIN_PERSONAL_TOKEN }} - download: - name: Download Translations - if: >- - github.repository_owner == 'mavlink' && - ( - github.event_name == 'schedule' || - (github.event_name == 'workflow_dispatch' && inputs.action == 'download') - ) - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: write - pull-requests: write - - steps: - - name: Harden Runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@v6 - - name: Download translations from Crowdin + if: steps.action.outputs.mode == 'download' uses: crowdin/github-action@v2 with: config: 'docs/crowdin_docs.yml' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 755b67166a7a..bebf5f1763cd 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -7,6 +7,12 @@ on: - 'android/**' - '**/*.gradle' - '**/*.gradle.kts' + - 'requirements*.txt' + - '**/requirements*.txt' + - 'pyproject.toml' + - '**/pyproject.toml' + - 'setup.py' + - '**/setup.py' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 17f616fb2d42..183f346614e7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -10,8 +10,6 @@ on: paths-ignore: - 'docs/**' pull_request: - paths-ignore: - - 'docs/**' workflow_dispatch: concurrency: @@ -29,31 +27,26 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' timeout-minutes: 5 + permissions: + contents: read outputs: - linux: ${{ steps.filter.outputs.linux }} - android: ${{ steps.filter.outputs.android }} - shared: ${{ steps.filter.outputs.shared }} + linux: ${{ steps.detect-linux.outputs.any }} + android: ${{ steps.detect-android.outputs.any }} steps: - - name: Detect changed paths - uses: dorny/paths-filter@v3 - id: filter + - name: Checkout repo + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Detect Linux changes + id: detect-linux + uses: ./.github/actions/detect-changes + with: + platform: docker-linux + - name: Detect Android changes + id: detect-android + uses: ./.github/actions/detect-changes with: - filters: | - shared: - - 'src/**' - - 'cmake/**' - - 'CMakeLists.txt' - - 'tools/setup/**' - - '.github/workflows/docker.yml' - - '.github/actions/docker/**' - - '.github/build-config.json' - linux: - - 'deploy/linux/**' - - 'deploy/docker/Dockerfile-build-ubuntu' - android: - - 'android/**' - - 'deploy/android/**' - - 'deploy/docker/Dockerfile-build-android' + platform: docker-android plan-builds: name: Plan Docker Builds @@ -65,57 +58,21 @@ jobs: matrix: ${{ steps.plan.outputs.matrix }} has_jobs: ${{ steps.plan.outputs.has_jobs }} steps: + - name: Checkout planner + uses: actions/checkout@v6 + with: + fetch-depth: 1 + sparse-checkout: | + .github/scripts/plan_docker_builds.py + sparse-checkout-cone-mode: false + - name: Plan matrix id: plan env: EVENT_NAME: ${{ github.event_name }} - SHARED: ${{ needs.changes.outputs.shared }} LINUX: ${{ needs.changes.outputs.linux }} ANDROID: ${{ needs.changes.outputs.android }} - run: | - python3 - <<'PY' - import json - import os - from pathlib import Path - - event_name = os.environ.get("EVENT_NAME", "") - shared = os.environ.get("SHARED", "") == "true" - linux_changed = os.environ.get("LINUX", "") == "true" - android_changed = os.environ.get("ANDROID", "") == "true" - - include: list[dict[str, object]] = [] - - linux_selected = event_name != "pull_request" or shared or linux_changed - android_selected = event_name != "pull_request" or shared or android_changed - - if linux_selected: - include.append( - { - "platform": "Linux", - "dockerfile": "Dockerfile-build-ubuntu", - "fuse": True, - "artifact_pattern": "*.AppImage", - } - ) - if android_selected: - include.append( - { - "platform": "Android", - "dockerfile": "Dockerfile-build-android", - "fuse": False, - "artifact_pattern": "*.apk", - } - ) - - matrix = {"include": include} - has_jobs = "true" if include else "false" - matrix_json = json.dumps(matrix, separators=(",", ":")) - - output_path = Path(os.environ["GITHUB_OUTPUT"]) - with output_path.open("a", encoding="utf-8") as f: - f.write(f"matrix={matrix_json}\n") - f.write(f"has_jobs={has_jobs}\n") - PY + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/plan_docker_builds.py" build: name: Docker ${{ matrix.platform }} diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index faf136d4ef86..d71e866f04fd 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -10,16 +10,6 @@ on: paths-ignore: - 'docs/**' pull_request: - paths: - - '.github/workflows/linux.yml' - - '.github/actions/**' - - 'deploy/linux/**' - - 'src/**' - - 'test/**' - - 'CMakeLists.txt' - - 'cmake/**' - - 'tools/setup/*debian*' - - 'tools/setup/install_dependencies.py' merge_group: workflow_dispatch: inputs: @@ -31,7 +21,6 @@ on: options: - Release - Debug - workflow_call: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -46,8 +35,30 @@ permissions: checks: write jobs: + changes: + name: Detect Changes + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + outputs: + should_build: ${{ steps.detect.outputs.any }} + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Detect changes + id: detect + uses: ./.github/actions/detect-changes + with: + platform: linux + build: name: Build ${{ matrix.arch }} ${{ matrix.build_type }} + needs: changes + if: always() && !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') runs-on: ${{ matrix.os }} timeout-minutes: 120 @@ -205,7 +216,8 @@ jobs: debug-validation: name: ${{ matrix.job_name }} - if: ${{ !cancelled() && (github.event_name != 'workflow_dispatch' || inputs.build_type == 'Debug') }} + needs: changes + if: ${{ !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') && (github.event_name != 'workflow_dispatch' || inputs.build_type == 'Debug') }} runs-on: ubuntu-22.04 timeout-minutes: ${{ matrix.timeout_minutes }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 69a7893b848b..c8e870471392 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -10,14 +10,6 @@ on: paths-ignore: - 'docs/**' pull_request: - paths: - - '.github/workflows/macos.yml' - - '.github/actions/**' - - 'deploy/macos/**' - - 'src/**' - - 'test/**' - - 'CMakeLists.txt' - - 'cmake/**' merge_group: workflow_dispatch: inputs: @@ -29,7 +21,6 @@ on: options: - Release - Debug - workflow_call: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -42,7 +33,29 @@ permissions: actions: read jobs: + changes: + name: Detect Changes + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + outputs: + should_build: ${{ steps.detect.outputs.any }} + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Detect changes + id: detect + uses: ./.github/actions/detect-changes + with: + platform: macos + build: + needs: changes + if: always() && !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') runs-on: macos-15 timeout-minutes: 120 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 66cae5648af6..3bccabbacc43 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -95,6 +95,8 @@ jobs: .github/actions/build-config .github/actions/qt-install .github/scripts/benchmark_runner.py + tools/common/build_config.py + tools/setup/setup_bootstrap.py tools/setup/read_config.py .github/build-config.json sparse-checkout-cone-mode: false diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9aedaf436668..e12126d719b6 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -5,15 +5,7 @@ on: branches: - master - 'Stable*' - paths: - - 'src/**' - - 'test/**' - - '.pre-commit-config.yaml' pull_request: - paths: - - 'src/**' - - 'test/**' - - '.pre-commit-config.yaml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -75,7 +67,7 @@ jobs: continue-on-error: true env: PRE_COMMIT_OUTPUT: pre-commit-output.txt - run: ./tools/pre-commit.sh --ci + run: python3 ./tools/pre_commit.py --ci - name: Prepare pre-commit results artifact if: github.event_name == 'pull_request' && always() @@ -86,38 +78,14 @@ jobs: SKIPPED: ${{ steps.pre-commit.outputs.skipped }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - mkdir -p pre-commit-results - - : "${EXIT_CODE:=1}" - : "${PASSED:=0}" - : "${FAILED:=0}" - : "${SKIPPED:=0}" - - python3 - <<'PY' - import json - import os - from pathlib import Path - - output_path = Path("pre-commit-results/pre-commit-results.json") - output_path.write_text( - json.dumps( - { - "exit_code": os.environ.get("EXIT_CODE", "1"), - "passed": os.environ.get("PASSED", "0"), - "failed": os.environ.get("FAILED", "0"), - "skipped": os.environ.get("SKIPPED", "0"), - "run_url": os.environ.get("RUN_URL", ""), - }, - indent=2, - ) - + "\n", - encoding="utf-8", - ) - PY - - if [[ -f pre-commit-output.txt ]]; then - cp pre-commit-output.txt pre-commit-results/pre-commit-output.txt - fi + python3 "${GITHUB_WORKSPACE}/.github/scripts/precommit_results.py" \ + --output-dir pre-commit-results \ + --exit-code "${EXIT_CODE:-1}" \ + --passed "${PASSED:-0}" \ + --failed "${FAILED:-0}" \ + --skipped "${SKIPPED:-0}" \ + --run-url "${RUN_URL}" \ + --output-file pre-commit-output.txt - name: Upload pre-commit results artifact if: github.event_name == 'pull_request' && always() diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d89a0f0434ce..ec00a84d237c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,11 @@ name: Release -# Disabled: semantic-release blocked by branch protection (can't push CHANGELOG.md) -# To fix: either remove @semantic-release/git plugin or use a PAT from allowed pusher +# TODO: Disabled — semantic-release blocked by branch protection (can't push CHANGELOG.md) +# Fix options: +# 1. Remove @semantic-release/git plugin (skip CHANGELOG commit) +# 2. Use a PAT from an allowed pusher +# 3. Use a GitHub App token with bypass permissions +# Until fixed, this workflow only runs via manual workflow_dispatch. on: workflow_dispatch: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 1eb2d135cf79..6d60e87e5ba3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -10,15 +10,6 @@ on: paths-ignore: - 'docs/**' pull_request: - paths: - - '.github/workflows/windows.yml' - - '.github/actions/**' - - 'deploy/windows/**' - - 'src/**' - - 'test/**' - - 'CMakeLists.txt' - - 'cmake/**' - - 'tools/setup/*windows*' merge_group: workflow_dispatch: inputs: @@ -30,7 +21,6 @@ on: options: - Release - Debug - workflow_call: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -43,8 +33,30 @@ permissions: actions: read jobs: + changes: + name: Detect Changes + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + outputs: + should_build: ${{ steps.detect.outputs.any }} + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Detect changes + id: detect + uses: ./.github/actions/detect-changes + with: + platform: windows + build: name: Build ${{ matrix.arch }} ${{ matrix.build_type }} + needs: changes + if: always() && !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') runs-on: ${{ matrix.os }} timeout-minutes: 120 @@ -81,10 +93,10 @@ jobs: shell: cmd steps: - # - name: Harden Runner - # uses: step-security/harden-runner@v2 - # with: - # egress-policy: audit + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit - name: Checkout repo uses: actions/checkout@v6 diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 000000000000..448339bafae5 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,9 @@ +extends: default + +rules: + document-start: disable + line-length: disable + comments-indentation: disable + truthy: + allowed-values: ["true", "false"] + check-keys: false diff --git a/THREAD_SAFETY_ANALYSIS.md b/THREAD_SAFETY_ANALYSIS.md deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/justfile b/justfile index 61c011cf0230..4cda8e484f4f 100644 --- a/justfile +++ b/justfile @@ -2,7 +2,7 @@ # Install: cargo install just, brew install just, or apt install just # Configuration from build-config.json -qt_version := `./tools/setup/read-config.sh qt_version 2>/dev/null || echo "6.10.1"` +qt_version := `python3 ./tools/setup/read_config.py --get qt_version 2>/dev/null || echo "6.10.1"` qt_dir := env_var_or_default("QT_DIR", home_directory() / "Qt" / qt_version / "gcc_64") build_type := env_var_or_default("BUILD_TYPE", "Debug") build_dir := "build" @@ -81,7 +81,7 @@ analyze: # Generate coverage report coverage: - ./tools/coverage.sh + python3 ./tools/coverage.py # Run lint + test check: lint test @@ -115,7 +115,7 @@ info: # Check dependency versions check-deps: - ./tools/check-deps.sh + python3 ./tools/check_deps.py # Clean all caches and build artifacts distclean: clean diff --git a/tools/README.md b/tools/README.md index 629a3e953a3f..640d0315a437 100644 --- a/tools/README.md +++ b/tools/README.md @@ -7,13 +7,13 @@ This directory contains development tools, scripts, and configuration files for ``` tools/ ├── analyze.py # Static analysis and formatting (clang-format, clang-tidy, cppcheck, clazy) -├── check-deps.sh # Check for outdated dependencies +├── check_deps.py # Check for outdated dependencies ├── clean.sh # Clean build artifacts and caches ├── configure.py # CMake configuration wrapper -├── coverage.sh # Code coverage reports -├── generate-docs.sh # Generate API docs (Doxygen) +├── coverage.py # Code coverage reports +├── generate_docs.py # Generate API docs (Doxygen) ├── param-docs.py # Generate parameter documentation -├── pre-commit.sh # Pre-commit hook runner +├── pre_commit.py # Pre-commit hook runner ├── release.sh # Semantic versioning and release automation ├── run_tests.py # Qt unit test runner ├── configs/ # Tool configuration files @@ -106,15 +106,15 @@ Clean build artifacts and caches. ./tools/clean.sh --dry-run # Show what would be removed ``` -### coverage.sh +### coverage.py Generate code coverage reports. Wrapper around CMake coverage targets. ```bash -./tools/coverage.sh # Build with coverage, run tests, generate report -./tools/coverage.sh --report # Generate report only (after tests) -./tools/coverage.sh --open # Generate and open in browser -./tools/coverage.sh --clean # Clean coverage data +python3 ./tools/coverage.py # Build with coverage, run tests, generate report +python3 ./tools/coverage.py --report # Generate report only (after tests) +python3 ./tools/coverage.py --open # Generate and open in browser +python3 ./tools/coverage.py --clean # Clean coverage data ``` Requires: `gcovr` (`pip install gcovr`) @@ -143,26 +143,26 @@ Profile QGC for performance and memory issues. ./tools/debuggers/profile.sh --sanitize # Build with AddressSanitizer ``` -### check-deps.sh +### check_deps.py Check for outdated dependencies and submodules. ```bash -./tools/check-deps.sh # Check all dependencies -./tools/check-deps.sh --submodules # Check git submodules only -./tools/check-deps.sh --qt # Check Qt version -./tools/check-deps.sh --update # Update submodules to latest +python3 ./tools/check_deps.py # Check all dependencies +python3 ./tools/check_deps.py --submodules # Check git submodules only +python3 ./tools/check_deps.py --qt # Check Qt version +python3 ./tools/check_deps.py --update # Update submodules to latest ``` -### generate-docs.sh +### generate_docs.py Generate API documentation using Doxygen. ```bash -./tools/generate-docs.sh # Generate HTML docs -./tools/generate-docs.sh --open # Generate and open in browser -./tools/generate-docs.sh --pdf # Generate PDF (requires LaTeX) -./tools/generate-docs.sh --clean # Clean generated docs +python3 ./tools/generate_docs.py # Generate HTML docs +python3 ./tools/generate_docs.py --open # Generate and open in browser +python3 ./tools/generate_docs.py --pdf # Generate PDF (requires LaTeX) +python3 ./tools/generate_docs.py --clean # Clean generated docs ``` Requires: `doxygen`, `graphviz` @@ -206,7 +206,7 @@ python3 ./tools/setup/install_dependencies.py --platform macos python .\tools\setup\install_dependencies.py --platform windows # Install Python tooling (pre-commit, test, coverage, etc.) -python3 ./tools/setup/install_python.py --groups precommit test coverage +python3 ./tools/setup/install_python.py precommit,test,coverage # Build GStreamer from source (Linux, optional) python3 ./tools/setup/build-gstreamer.py --platform linux --prefix /opt/gstreamer diff --git a/tools/_bootstrap.py b/tools/_bootstrap.py new file mode 100644 index 000000000000..22dd025181d7 --- /dev/null +++ b/tools/_bootstrap.py @@ -0,0 +1,18 @@ +"""Shared import bootstrap for Python entrypoints under tools/.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def ensure_tools_dir(start: str | Path) -> Path: + """Ensure the top-level ``tools`` directory is importable.""" + path = Path(start).resolve() + current = path if path.is_dir() else path.parent + for candidate in [current] + list(current.parents): + if candidate.name == "tools": + if str(candidate) not in sys.path: + sys.path.insert(0, str(candidate)) + return candidate + raise RuntimeError(f"Could not locate tools directory from {start}") diff --git a/tools/analyze.py b/tools/analyze.py index 6920d973cc2d..9443fed478f8 100755 --- a/tools/analyze.py +++ b/tools/analyze.py @@ -199,7 +199,8 @@ def run(self, files: list[Path], fix: bool = False) -> AnalysisResult: capture_output=True, text=True, ) - version_match = version_result.stdout.split()[2] if version_result.stdout else "unknown" + parts = version_result.stdout.split() if version_result.stdout else [] + version_match = parts[2] if len(parts) > 2 else "unknown" log_info(f"Using clang-format version {version_match}") if not files: diff --git a/tools/check-deps.sh b/tools/check-deps.sh deleted file mode 100755 index 33c0e63df85a..000000000000 --- a/tools/check-deps.sh +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env bash -# -# Check for outdated dependencies and submodules -# -# Usage: -# ./tools/check-deps.sh # Check all dependencies -# ./tools/check-deps.sh --submodules # Check only git submodules -# ./tools/check-deps.sh --qt # Check Qt version -# ./tools/check-deps.sh --update # Update submodules to latest -# -# Checks: -# - Git submodules vs upstream -# - Qt version vs latest -# - GStreamer version vs latest -# - Python dependencies - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } -log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } - -# Defaults -CHECK_ALL=true -CHECK_SUBMODULES=false -CHECK_QT=false -UPDATE_DEPS=false - -show_help() { - head -15 "$0" | tail -13 - exit 0 -} - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - show_help - ;; - --submodules) - CHECK_ALL=false - CHECK_SUBMODULES=true - shift - ;; - --qt) - CHECK_ALL=false - CHECK_QT=true - shift - ;; - --update) - UPDATE_DEPS=true - shift - ;; - *) - log_error "Unknown option: $1" - exit 1 - ;; - esac -done - -check_submodules() { - log_info "Checking git submodules..." - - cd "$REPO_ROOT" - - # Initialize submodules if needed - git submodule update --init --recursive 2>/dev/null || true - - local outdated=0 - - while IFS= read -r line; do - if [[ -z "$line" ]]; then - continue - fi - - # Parse submodule status (status and hash reserved for future use) - local _status="${line:0:1}" - local _hash="${line:1:40}" - local path - path=$(echo "$line" | awk '{print $2}') - - if [[ ! -d "$REPO_ROOT/$path" ]]; then - continue - fi - - cd "$REPO_ROOT/$path" - - # Get current and remote info (reserved for future verbose output) - local _current_hash _branch - _current_hash=$(git rev-parse HEAD 2>/dev/null || echo "unknown") - _branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached") - - # Fetch updates quietly - git fetch --quiet 2>/dev/null || true - - # Check if behind - local behind=0 - # shellcheck disable=SC1083 # @{u} is valid git syntax for upstream - if git rev-parse '@{u}' &>/dev/null; then - behind=$(git rev-list --count 'HEAD..@{u}' 2>/dev/null || echo "0") - fi - - if [[ "$behind" -gt 0 ]]; then - log_warn "$path: $behind commits behind upstream" - ((outdated++)) || true - else - echo -e " ${GREEN}✓${NC} $path (up to date)" - fi - - cd "$REPO_ROOT" - done < <(git submodule status --recursive 2>/dev/null) - - if [[ "$outdated" -eq 0 ]]; then - log_ok "All submodules up to date" - else - log_warn "$outdated submodule(s) have updates available" - if [[ "$UPDATE_DEPS" == true ]]; then - log_info "Updating submodules..." - git submodule update --remote --merge - log_ok "Submodules updated" - else - log_info "Run with --update to update submodules" - fi - fi -} - -check_qt_version() { - log_info "Checking Qt version..." - - # Read current version from config - local config_file="$REPO_ROOT/.github/build-config.json" - if [[ ! -f "$config_file" ]]; then - log_warn "build-config.json not found" - return - fi - - local current_version - current_version=$(python3 -c "import json; print(json.load(open('$config_file')).get('qt_version', 'unknown'))") - - echo " Current: Qt $current_version" - - # Check latest Qt version (from qt.io) - local latest_info - if command -v curl &> /dev/null; then - # Try to get latest version info - # Extract Qt 6.x versions (portable grep) - latest_info=$(curl -s "https://download.qt.io/official_releases/qt/" 2>/dev/null | \ - grep -Eo '6\.[0-9]+' | sort -V | tail -1 || echo "") - - if [[ -n "$latest_info" ]]; then - echo " Latest minor: Qt $latest_info.x" - - local current_minor="${current_version%.*}" - if [[ "$current_minor" != "$latest_info" ]]; then - log_warn "Newer Qt minor version available: $latest_info" - else - log_ok "Using latest Qt minor version" - fi - fi - fi - - # Check installed Qt - if command -v qmake &> /dev/null; then - local installed - installed=$(qmake --version 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' || echo "not found") - echo " Installed: Qt $installed" - elif [[ -n "${QT_ROOT_DIR:-}" ]]; then - echo " QT_ROOT_DIR: $QT_ROOT_DIR" - fi -} - -check_gstreamer_version() { - log_info "Checking GStreamer version..." - - # Read current version from config - local config_file="$REPO_ROOT/.github/build-config.json" - if [[ ! -f "$config_file" ]]; then - return - fi - - local current_version - current_version=$(python3 -c "import json; print(json.load(open('$config_file')).get('gstreamer_version', 'unknown'))") - - echo " Configured: GStreamer $current_version" - - # Check installed version - if command -v gst-launch-1.0 &> /dev/null; then - local installed - installed=$(gst-launch-1.0 --version 2>/dev/null | head -1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' || echo "not found") - echo " Installed: GStreamer $installed" - fi -} - -check_python_deps() { - log_info "Checking Python dependencies..." - - local req_files=("requirements.txt" "docs/requirements.txt") - - for req in "${req_files[@]}"; do - if [[ -f "$REPO_ROOT/$req" ]]; then - echo " Checking $req..." - if command -v pip &> /dev/null; then - # Check for outdated packages - local outdated - outdated=$(pip list --outdated --format=columns 2>/dev/null | tail -n +3 || echo "") - if [[ -n "$outdated" ]]; then - echo "$outdated" | while read -r line; do - echo " $line" - done - fi - fi - fi - done -} - -check_build_tools() { - log_info "Checking build tools..." - - local tools=("cmake" "ninja" "ccache" "clang-format" "clang-tidy") - - for tool in "${tools[@]}"; do - if command -v "$tool" &> /dev/null; then - local version - version=$("$tool" --version 2>/dev/null | head -1 || echo "unknown") - echo -e " ${GREEN}✓${NC} $tool: $version" - else - echo -e " ${YELLOW}✗${NC} $tool: not installed" - fi - done -} - -# Main -cd "$REPO_ROOT" - -if [[ "$CHECK_ALL" == true ]]; then - check_submodules - echo "" - check_qt_version - echo "" - check_gstreamer_version - echo "" - check_build_tools -elif [[ "$CHECK_SUBMODULES" == true ]]; then - check_submodules -elif [[ "$CHECK_QT" == true ]]; then - check_qt_version -fi - -echo "" -log_ok "Dependency check complete" diff --git a/tools/check_deps.py b/tools/check_deps.py new file mode 100644 index 000000000000..82961db05a2e --- /dev/null +++ b/tools/check_deps.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Check dependency and tool versions used by QGroundControl development.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from pathlib import Path +from urllib.error import URLError +from urllib.request import urlopen + +from _bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.build_config import get_build_config_value +from common.logging import log_error, log_info, log_ok, log_warn + +QT_RELEASES_URL = "https://download.qt.io/official_releases/qt/" +REQ_FILES = [ + Path("requirements.txt"), + Path("docs/requirements.txt"), + Path("tools/pyproject.toml"), +] +BUILD_TOOLS = ["cmake", "ninja", "ccache", "clang-format", "clang-tidy"] + + +def run_git(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]: + """Run a git command and capture stdout/stderr.""" + return subprocess.run( + ["git", *args], + cwd=cwd, + capture_output=True, + text=True, + check=False, + ) + + +def parse_submodule_paths(repo_root: Path) -> list[Path]: + """Return existing submodule paths from ``git submodule status``.""" + result = run_git(["submodule", "status", "--recursive"], repo_root) + if result.returncode != 0: + return [] + + paths: list[Path] = [] + for line in result.stdout.splitlines(): + parts = line.strip().split() + if len(parts) < 2: + continue + path = repo_root / parts[1] + if path.is_dir(): + paths.append(path) + return paths + + +def check_submodules(repo_root: Path, *, update: bool) -> None: + """Check whether git submodules are behind their upstream branch.""" + log_info("Checking git submodules...") + run_git(["submodule", "update", "--init", "--recursive"], repo_root) + + outdated: list[tuple[Path, int]] = [] + for submodule in parse_submodule_paths(repo_root): + run_git(["fetch", "--quiet"], submodule) + upstream = run_git(["rev-parse", "@{u}"], submodule) + if upstream.returncode != 0: + print(f" - {submodule.relative_to(repo_root)} (no upstream)") + continue + + behind_result = run_git(["rev-list", "--count", "HEAD..@{u}"], submodule) + behind = int(behind_result.stdout.strip() or "0") if behind_result.returncode == 0 else 0 + if behind > 0: + outdated.append((submodule, behind)) + log_warn(f"{submodule.relative_to(repo_root)}: {behind} commits behind upstream") + else: + print(f" - {submodule.relative_to(repo_root)} (up to date)") + + if not outdated: + log_ok("All submodules up to date") + return + + log_warn(f"{len(outdated)} submodule(s) have updates available") + if update: + log_info("Updating submodules...") + result = run_git(["submodule", "update", "--remote", "--merge"], repo_root) + if result.returncode == 0: + log_ok("Submodules updated") + else: + log_error("Submodule update failed") + if result.stderr: + print(result.stderr.strip(), file=sys.stderr) + else: + log_info("Run with --update to update submodules") + + +def fetch_latest_qt_minor() -> str | None: + """Return the latest Qt 6 minor version available on download.qt.io.""" + try: + with urlopen(QT_RELEASES_URL, timeout=15) as response: + body = response.read().decode("utf-8", errors="ignore") + except (OSError, URLError): + return None + + versions = re.findall(r">6\.(\d+)/<", body) + if not versions: + versions = re.findall(r"6\.(\d+)", body) + if not versions: + return None + latest_minor = max(int(version) for version in versions) + return f"6.{latest_minor}" + + +def check_qt_version() -> None: + """Check configured and installed Qt versions.""" + log_info("Checking Qt version...") + current_version = get_build_config_value("qt_version", "unknown", start=Path(__file__).resolve()) + print(f" Current: Qt {current_version}") + + latest_minor = fetch_latest_qt_minor() + if latest_minor: + print(f" Latest minor: Qt {latest_minor}.x") + if current_version and current_version != "unknown": + current_minor = ".".join(current_version.split(".")[:2]) + if current_minor != latest_minor: + log_warn(f"Newer Qt minor version available: {latest_minor}") + else: + log_ok("Using latest Qt minor version") + + for candidate in (["qmake", "--version"], ["qtpaths", "--qt-version"]): + result = subprocess.run(candidate, capture_output=True, text=True, check=False) + if result.returncode != 0: + continue + match = re.search(r"\d+\.\d+\.\d+", result.stdout) + if match: + print(f" Installed: Qt {match.group(0)}") + return + + +def check_gstreamer_version() -> None: + """Check configured and installed GStreamer versions.""" + log_info("Checking GStreamer version...") + current_version = get_build_config_value( + "gstreamer_version", + "unknown", + start=Path(__file__).resolve(), + ) + print(f" Configured: GStreamer {current_version}") + + result = subprocess.run( + ["gst-launch-1.0", "--version"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return + match = re.search(r"\d+\.\d+\.\d+", result.stdout) + if match: + print(f" Installed: GStreamer {match.group(0)}") + + +def find_python_manifests(repo_root: Path) -> list[Path]: + """Return dependency manifest files present in the repo.""" + return [path for path in REQ_FILES if (repo_root / path).exists()] + + +def check_python_deps(repo_root: Path) -> None: + """Check installed Python packages for available updates.""" + log_info("Checking Python dependencies...") + manifests = find_python_manifests(repo_root) + if not manifests: + log_warn("No Python dependency manifests found") + return + + print(" Manifests:") + for manifest in manifests: + print(f" - {manifest}") + + result = subprocess.run( + [sys.executable, "-m", "pip", "list", "--outdated", "--format=json"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + log_warn("Could not query installed Python package updates") + return + + try: + outdated = json.loads(result.stdout or "[]") + except json.JSONDecodeError: + log_warn("Could not parse pip outdated output") + return + + if not outdated: + log_ok("No outdated installed Python packages reported") + return + + print(" Outdated installed packages:") + for package in outdated: + name = package.get("name", "unknown") + current = package.get("version", "?") + latest = package.get("latest_version", "?") + print(f" - {name}: {current} -> {latest}") + + +def check_build_tools() -> None: + """Print installed build tool versions.""" + log_info("Checking build tools...") + for tool in BUILD_TOOLS: + result = subprocess.run([tool, "--version"], capture_output=True, text=True, check=False) + if result.returncode != 0: + print(f" - {tool}: not installed") + continue + first_line = result.stdout.splitlines()[0] if result.stdout else "unknown" + print(f" - {tool}: {first_line}") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Check dependency versions and tool availability.") + parser.add_argument("--submodules", action="store_true", help="Check only git submodules") + parser.add_argument("--qt", action="store_true", help="Check only Qt version") + parser.add_argument("--update", action="store_true", help="Update submodules to latest upstream") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Run the requested dependency checks.""" + args = parse_args(argv) + repo_root = Path(__file__).resolve().parent.parent + + check_all = not args.submodules and not args.qt + if check_all or args.submodules: + check_submodules(repo_root, update=args.update) + if check_all: + print() + check_qt_version() + print() + check_gstreamer_version() + print() + check_python_deps(repo_root) + print() + check_build_tools() + elif args.qt: + check_qt_version() + + print() + log_ok("Dependency check complete") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/clean.sh b/tools/clean.sh index 9dde8c0dd343..37592722d7ea 100755 --- a/tools/clean.sh +++ b/tools/clean.sh @@ -18,18 +18,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } -log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*"; } +source "$REPO_ROOT/tools/common/shell-utils.sh" # Defaults CLEAN_ALL=false diff --git a/tools/common/__init__.py b/tools/common/__init__.py index 7f47d8497c68..5798fdcfe815 100644 --- a/tools/common/__init__.py +++ b/tools/common/__init__.py @@ -34,6 +34,22 @@ parse_json_documents, list_workflow_runs_for_sha, list_run_artifacts, + write_github_output, +) + +from .build_config import ( + find_build_config, + load_build_config, + get_build_config_value, + export_build_config_values, + derive_ios_qt_modules, +) + +from .github_runs import ( + parse_created_at, + is_newer_run, + select_latest_runs_by_name, + group_runs_by_name, ) from .logging import ( @@ -89,6 +105,16 @@ 'parse_json_documents', 'list_workflow_runs_for_sha', 'list_run_artifacts', + 'write_github_output', + 'find_build_config', + 'load_build_config', + 'get_build_config_value', + 'export_build_config_values', + 'derive_ios_qt_modules', + 'parse_created_at', + 'is_newer_run', + 'select_latest_runs_by_name', + 'group_runs_by_name', # Logging 'Color', 'Colors', diff --git a/tools/common/build_config.py b/tools/common/build_config.py new file mode 100644 index 000000000000..da2a8df700a4 --- /dev/null +++ b/tools/common/build_config.py @@ -0,0 +1,120 @@ +"""Shared helpers for reading and exporting .github/build-config.json.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + +CONFIG_ENV_VAR = "CONFIG_FILE" +CONFIG_RELATIVE_PATH = Path(".github") / "build-config.json" +DEFAULT_EXPORT_KEYS = [ + "qt_version", + "qt_minimum_version", + "qt_modules", + "gstreamer_minimum_version", + "gstreamer_version", + "gstreamer_macos_version", + "gstreamer_android_version", + "gstreamer_windows_version", + "xcode_version", + "xcode_ios_version", + "ndk_version", + "ndk_full_version", + "java_version", + "android_platform", + "android_min_sdk", + "android_build_tools", + "android_cmdline_tools", + "cmake_minimum_version", + "macos_deployment_target", + "ios_deployment_target", +] +IOS_QT_MODULE_EXCLUDES = {"qtserialport", "qtscxml"} + + +def find_build_config( + start: Path | None = None, + *, + extra_candidates: list[Path] | None = None, +) -> Path: + """Find build-config.json by env override, extra candidates, or repo search.""" + if env_path := os.environ.get(CONFIG_ENV_VAR): + return Path(env_path) + + for candidate in extra_candidates or []: + if candidate.exists(): + return candidate + + current = (start or Path(__file__).resolve()).resolve() + if current.is_file(): + current = current.parent + + while current != current.parent: + config_path = current / CONFIG_RELATIVE_PATH + if config_path.exists(): + return config_path + current = current.parent + + raise FileNotFoundError(f"Could not find {CONFIG_RELATIVE_PATH}") + + +def load_build_config( + config_file: Path | None = None, + *, + start: Path | None = None, + extra_candidates: list[Path] | None = None, +) -> dict[str, Any]: + """Load and parse the build config JSON file.""" + path = config_file or find_build_config(start, extra_candidates=extra_candidates) + with path.open(encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, dict): + raise ValueError(f"Expected JSON object in {path}") + return data + + +def get_build_config_value( + key: str, + default: str = "", + *, + config_file: Path | None = None, + start: Path | None = None, + extra_candidates: list[Path] | None = None, +) -> str: + """Return a string value from the build config or *default*.""" + try: + config = load_build_config(config_file, start=start, extra_candidates=extra_candidates) + except (FileNotFoundError, OSError, json.JSONDecodeError, ValueError): + return default + value = config.get(key, default) + return str(value) if value is not None else default + + +def derive_ios_qt_modules(qt_modules: str) -> str: + """Return the iOS-safe Qt module list.""" + modules = [module for module in qt_modules.split() if module not in IOS_QT_MODULE_EXCLUDES] + return " ".join(modules) + + +def export_build_config_values( + config: dict[str, Any], + *, + keys: list[str] | None = None, +) -> dict[str, str]: + """Return uppercase env-style values exported from the config.""" + exported: dict[str, str] = {} + for key in keys or DEFAULT_EXPORT_KEYS: + if key in config: + exported[key.upper()] = str(config[key]) + return exported + + +def format_github_output(values: dict[str, str]) -> str: + """Format values for writing to $GITHUB_OUTPUT.""" + lines = [f"{key.lower()}={value}" for key, value in sorted(values.items())] + qt_modules = values.get("QT_MODULES", "") + if qt_modules: + lines.append(f"qt_modules_ios={derive_ios_qt_modules(qt_modules)}") + return "\n".join(lines) diff --git a/tools/common/gh_actions.py b/tools/common/gh_actions.py index f30aab74e132..de3d434574fd 100644 --- a/tools/common/gh_actions.py +++ b/tools/common/gh_actions.py @@ -12,7 +12,6 @@ from urllib import parse as urllib_parse from urllib import request as urllib_request - def gh(*args: str, check: bool = True) -> subprocess.CompletedProcess: """Run a gh CLI command and return the process result.""" return subprocess.run( @@ -181,3 +180,12 @@ def list_run_artifacts(repo: str, run_id: int | str) -> list[dict[str, Any]]: if isinstance(items, list): artifacts.extend(item for item in items if isinstance(item, dict)) return artifacts + + +def write_github_output(outputs: dict[str, str]) -> None: + """Write key=value pairs to $GITHUB_OUTPUT for GitHub Actions.""" + github_output = os.environ.get('GITHUB_OUTPUT') + if github_output: + with open(github_output, 'a', encoding="utf-8") as f: + for key, value in outputs.items(): + f.write(f"{key}={value}\n") diff --git a/tools/common/github_runs.py b/tools/common/github_runs.py new file mode 100644 index 000000000000..f29093708015 --- /dev/null +++ b/tools/common/github_runs.py @@ -0,0 +1,90 @@ +"""Shared helpers for selecting and grouping GitHub workflow runs.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any + + +def parse_created_at(created_at: Any) -> datetime | None: + """Parse GitHub ISO-8601 timestamps into datetimes.""" + value = str(created_at).strip() + if not value: + return None + if value.endswith("Z"): + value = f"{value[:-1]}+00:00" + try: + return datetime.fromisoformat(value) + except ValueError: + return None + + +def is_newer_run(candidate: dict[str, Any], existing: dict[str, Any]) -> bool: + """Return True when *candidate* is newer than *existing*.""" + candidate_created_at = str(candidate.get("created_at", "")) + existing_created_at = str(existing.get("created_at", "")) + candidate_dt = parse_created_at(candidate_created_at) + existing_dt = parse_created_at(existing_created_at) + if candidate_dt is not None and existing_dt is not None: + return candidate_dt > existing_dt + return candidate_created_at > existing_created_at + + +def select_latest_runs_by_name( + runs: list[dict[str, Any]], + names: set[str], + *, + event: str = "", + status: str = "", + conclusion: str = "", +) -> dict[str, dict[str, Any]]: + """Return the latest workflow run per name after optional filtering.""" + latest: dict[str, dict[str, Any]] = {} + for run in runs: + name = str(run.get("name", "")) + if name not in names: + continue + if event and str(run.get("event", "")) != event: + continue + if status and str(run.get("status", "")) != status: + continue + if conclusion and str(run.get("conclusion", "")) != conclusion: + continue + existing = latest.get(name) + if existing is None or is_newer_run(run, existing): + latest[name] = run + return latest + + +def group_runs_by_name( + runs: list[dict[str, Any]], + names: list[str], + *, + event: str = "", + status: str = "", + conclusion: str = "", +) -> dict[str, list[dict[str, Any]]]: + """Group workflow runs by name after optional filtering, newest first.""" + target = set(names) + grouped: dict[str, list[dict[str, Any]]] = {name: [] for name in names} + for run in runs: + name = str(run.get("name", "")) + if name not in target: + continue + if event and str(run.get("event", "")) != event: + continue + if status and str(run.get("status", "")) != status: + continue + if conclusion and str(run.get("conclusion", "")) != conclusion: + continue + grouped[name].append(run) + + for name in grouped: + grouped[name].sort( + key=lambda run: ( + parse_created_at(run.get("created_at")) is not None, + str(run.get("created_at", "")), + ), + reverse=True, + ) + return grouped diff --git a/tools/common/shell-utils.sh b/tools/common/shell-utils.sh new file mode 100644 index 000000000000..591741656f04 --- /dev/null +++ b/tools/common/shell-utils.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Shared shell utilities for QGC developer tools. +# Source this file at the top of shell scripts: +# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common/shell-utils.sh" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } diff --git a/tools/coverage.py b/tools/coverage.py new file mode 100644 index 000000000000..01b004cdb34a --- /dev/null +++ b/tools/coverage.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Generate code coverage reports for QGroundControl.""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +from _bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.logging import log_error, log_info, log_ok + +LINE_COVERAGE_RE = re.compile(r"lines:\s*(.+)") +BRANCH_COVERAGE_RE = re.compile(r"branches:\s*(.+)") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Build and report test coverage.") + parser.add_argument( + "--mode", + choices=["full", "report-only"], + default="full", + help="Coverage generation mode (default: full)", + ) + parser.add_argument("-r", "--report", action="store_true", help="Generate report only") + parser.add_argument("-o", "--open", dest="open_report", action="store_true", help="Open HTML report") + parser.add_argument("-c", "--clean", action="store_true", help="Clean coverage data") + parser.add_argument("--xml", action="store_true", help="Generate coverage report and highlight XML output path") + parser.add_argument("--log-file", default="", help="Write coverage target output to a file") + parser.add_argument( + "--step-summary", + action="store_true", + help="Append a coverage summary to GITHUB_STEP_SUMMARY if available", + ) + parser.add_argument( + "-b", + "--build-dir", + default="build-coverage", + help="Coverage build directory (default: build-coverage)", + ) + return parser.parse_args(argv) + + +def run_command( + cmd: list[str], + *, + cwd: Path | None = None, + capture_output: bool = False, +) -> subprocess.CompletedProcess[str]: + """Run a subprocess and fail fast on errors.""" + return subprocess.run(cmd, cwd=cwd, check=True, capture_output=capture_output, text=True) + + +def check_dependencies() -> None: + """Ensure required tooling is installed.""" + if shutil.which("gcovr") is None: + raise RuntimeError("gcovr not found. Install with: pip install gcovr") + + +def clean_coverage(build_dir: Path) -> None: + """Remove coverage build artifacts.""" + log_info("Cleaning coverage data...") + shutil.rmtree(build_dir, ignore_errors=True) + log_ok("Coverage data cleaned") + + +def configure_build(repo_root: Path, build_dir: Path) -> None: + """Configure the coverage build directory.""" + cache_path = build_dir / "CMakeCache.txt" + if cache_path.exists(): + cache_text = cache_path.read_text(encoding="utf-8", errors="ignore") + if "QGC_ENABLE_COVERAGE:BOOL=ON" in cache_text: + log_info("Using existing coverage build configuration") + return + + log_info("Configuring build with coverage...") + run_command( + [ + "cmake", + "-B", + str(build_dir), + "-S", + str(repo_root), + "-DCMAKE_BUILD_TYPE=Debug", + "-DQGC_ENABLE_COVERAGE=ON", + "-DQGC_BUILD_TESTING=ON", + "-G", + "Ninja", + ] + ) + log_ok("Build configured") + + +def build_project(build_dir: Path) -> None: + """Build the coverage target.""" + log_info("Building project...") + run_command(["cmake", "--build", str(build_dir), "--parallel"]) + log_ok("Build complete") + + +def run_tests(build_dir: Path) -> None: + """Run tests and fail if any test fails.""" + log_info("Running tests...") + run_command(["ctest", "--test-dir", str(build_dir), "--output-on-failure", "--timeout", "300"]) + + +def build_step_summary(log_text: str, mode: str) -> str: + """Build a GitHub Step Summary section from gcovr output.""" + lines = ["## Code Coverage", "", f"Mode: `{mode}`", ""] + line_match = LINE_COVERAGE_RE.search(log_text) + branch_match = BRANCH_COVERAGE_RE.search(log_text) + if line_match: + lines.extend( + [ + "| Metric | Coverage |", + "|--------|----------|", + f"| Lines | {line_match.group(1).strip()} |", + f"| Branches | {(branch_match.group(1).strip() if branch_match else 'N/A')} |", + "", + "[View detailed report](coverage.html)", + ] + ) + else: + lines.append("Coverage data not available") + return "\n".join(lines) + "\n" + + +def maybe_write_step_summary(log_text: str, mode: str) -> None: + """Append coverage markdown to GITHUB_STEP_SUMMARY when requested.""" + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + return + with open(summary_path, "a", encoding="utf-8") as handle: + handle.write(build_step_summary(log_text, mode)) + + +def generate_report(build_dir: Path, *, xml_only: bool, log_file: Path | None = None, mode: str = "full") -> str: + """Generate coverage artifacts from existing coverage data.""" + log_info("Generating coverage report...") + result = run_command( + ["cmake", "--build", str(build_dir), "--target", "coverage-report"], + capture_output=True, + ) + if result.stdout: + print(result.stdout, end="") + if result.stderr: + print(result.stderr, end="", file=sys.stderr) + if log_file is not None: + log_file.write_text((result.stdout or "") + (result.stderr or ""), encoding="utf-8") + print() + log_ok("Coverage report generated") + log_info(f" XML: {build_dir / 'coverage.xml'}") + if not xml_only: + log_info(f" HTML: {build_dir / 'coverage.html'}") + return (result.stdout or "") + (result.stderr or "") + + +def open_report(build_dir: Path) -> None: + """Open the generated HTML coverage report.""" + report = build_dir / "coverage.html" + if not report.exists(): + raise FileNotFoundError(f"Report not found: {report}") + + log_info("Opening coverage report...") + opener = shutil.which("xdg-open") or shutil.which("open") + if opener is None: + raise RuntimeError(f"Could not find a browser opener. Report at: {report}") + subprocess.run([opener, str(report)], check=False) + + +def main(argv: list[str] | None = None) -> int: + """Run the requested coverage workflow.""" + args = parse_args(argv) + repo_root = Path(__file__).resolve().parent.parent + build_dir = Path(args.build_dir) + mode = "report-only" if args.report else args.mode + if not build_dir.is_absolute(): + build_dir = repo_root / build_dir + log_file = Path(args.log_file) if args.log_file else None + if log_file is not None and not log_file.is_absolute(): + log_file = repo_root / log_file + + try: + check_dependencies() + if args.clean: + clean_coverage(build_dir) + return 0 + + if mode == "report-only": + report_output = generate_report(build_dir, xml_only=args.xml, log_file=log_file, mode=mode) + else: + configure_build(repo_root, build_dir) + build_project(build_dir) + run_tests(build_dir) + report_output = generate_report(build_dir, xml_only=args.xml, log_file=log_file, mode=mode) + + if args.open_report: + open_report(build_dir) + if args.step_summary: + maybe_write_step_summary(report_output, mode) + return 0 + except (RuntimeError, FileNotFoundError, subprocess.CalledProcessError) as exc: + log_error(str(exc)) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/coverage.sh b/tools/coverage.sh deleted file mode 100755 index 3f8df907a96f..000000000000 --- a/tools/coverage.sh +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env bash -# -# Generate code coverage reports for QGroundControl -# -# Usage: -# ./tools/coverage.sh # Build with coverage and run tests -# ./tools/coverage.sh --report # Generate report only (after tests) -# ./tools/coverage.sh --open # Generate and open report in browser -# ./tools/coverage.sh --clean # Clean coverage data -# ./tools/coverage.sh --xml # Generate XML only (for CI tools) -# -# Requirements: -# - gcovr (pip install gcovr) -# -# This script wraps CMake coverage targets defined in cmake/modules/Coverage.cmake - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } -log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } - -# Defaults -BUILD_DIR="$REPO_ROOT/build-coverage" -REPORT_ONLY=false -OPEN_REPORT=false -CLEAN_ONLY=false -XML_ONLY=false - -show_help() { - head -14 "$0" | tail -12 - exit 0 -} - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - show_help - ;; - -r|--report) - REPORT_ONLY=true - shift - ;; - -o|--open) - OPEN_REPORT=true - shift - ;; - -c|--clean) - CLEAN_ONLY=true - shift - ;; - --xml) - XML_ONLY=true - shift - ;; - -b|--build-dir) - BUILD_DIR="$2" - shift 2 - ;; - *) - log_error "Unknown option: $1" - exit 1 - ;; - esac -done - -check_dependencies() { - if ! command -v gcovr &> /dev/null; then - log_error "gcovr not found" - log_info "Install with: pip install gcovr" - exit 1 - fi -} - -clean_coverage() { - log_info "Cleaning coverage data..." - rm -rf "$BUILD_DIR" - log_ok "Coverage data cleaned" -} - -configure_build() { - if [[ -f "$BUILD_DIR/CMakeCache.txt" ]]; then - # Check if already configured with coverage - if grep -q "QGC_ENABLE_COVERAGE:BOOL=ON" "$BUILD_DIR/CMakeCache.txt" 2>/dev/null; then - log_info "Using existing coverage build configuration" - return - fi - fi - - log_info "Configuring build with coverage..." - cmake -B "$BUILD_DIR" -S "$REPO_ROOT" \ - -DCMAKE_BUILD_TYPE=Debug \ - -DQGC_ENABLE_COVERAGE=ON \ - -DQGC_BUILD_TESTING=ON \ - -G Ninja - - log_ok "Build configured" -} - -build_project() { - log_info "Building project..." - cmake --build "$BUILD_DIR" --parallel - log_ok "Build complete" -} - -run_tests() { - log_info "Running tests..." - # Run via ctest for proper test discovery - ctest --test-dir "$BUILD_DIR" --output-on-failure --timeout 300 || true - log_ok "Tests complete" -} - -generate_report() { - log_info "Generating coverage report..." - - if [[ "$XML_ONLY" == true ]]; then - cmake --build "$BUILD_DIR" --target coverage-report - else - cmake --build "$BUILD_DIR" --target coverage-report - fi - - echo "" - log_ok "Coverage report generated:" - log_info " HTML: $BUILD_DIR/coverage.html" - log_info " XML: $BUILD_DIR/coverage.xml" -} - -open_report() { - local report="$BUILD_DIR/coverage.html" - if [[ ! -f "$report" ]]; then - log_error "Report not found. Run coverage first." - exit 1 - fi - - log_info "Opening coverage report..." - - if command -v xdg-open &> /dev/null; then - xdg-open "$report" - elif command -v open &> /dev/null; then - open "$report" - else - log_warn "Could not open browser. Report at: $report" - fi -} - -# Main -cd "$REPO_ROOT" -check_dependencies - -if [[ "$CLEAN_ONLY" == true ]]; then - clean_coverage - exit 0 -fi - -if [[ "$REPORT_ONLY" == true ]]; then - generate_report - if [[ "$OPEN_REPORT" == true ]]; then - open_report - fi - exit 0 -fi - -# Full coverage workflow -configure_build -build_project -run_tests -generate_report - -if [[ "$OPEN_REPORT" == true ]]; then - open_report -fi - -log_ok "Coverage complete!" diff --git a/tools/debuggers/profile.sh b/tools/debuggers/profile.sh index 1fa225bdac53..2cdbea93f0aa 100755 --- a/tools/debuggers/profile.sh +++ b/tools/debuggers/profile.sh @@ -23,19 +23,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } -log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +source "$REPO_ROOT/tools/common/shell-utils.sh" # Defaults BUILD_DIR="$REPO_ROOT/build" @@ -122,8 +111,8 @@ run_memcheck() { --track-origins=yes \ --verbose \ --log-file="$output" \ - --suppressions="$REPO_ROOT/tools/debuggers/valgrind.supp" 2>/dev/null || true \ - "$EXECUTABLE" $EXTRA_ARGS + --suppressions="$REPO_ROOT/tools/debuggers/valgrind.supp" \ + "$EXECUTABLE" $EXTRA_ARGS || true log_ok "Memcheck complete. Results: $output" diff --git a/tools/generate-docs.sh b/tools/generate-docs.sh deleted file mode 100755 index a879d9f8493a..000000000000 --- a/tools/generate-docs.sh +++ /dev/null @@ -1,239 +0,0 @@ -#!/usr/bin/env bash -# -# Generate API documentation using Doxygen -# -# Usage: -# ./tools/generate-docs.sh # Generate HTML docs -# ./tools/generate-docs.sh --open # Generate and open in browser -# ./tools/generate-docs.sh --pdf # Generate PDF (requires LaTeX) -# ./tools/generate-docs.sh --clean # Clean generated docs -# -# Requirements: -# - doxygen -# - graphviz (for diagrams) -# - Optional: texlive (for PDF output) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } -log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } - -# Defaults -OUTPUT_DIR="$REPO_ROOT/docs/api" -DOXYFILE="$REPO_ROOT/Doxyfile" -GENERATE_PDF=false -OPEN_DOCS=false -CLEAN_ONLY=false - -show_help() { - head -14 "$0" | tail -12 - exit 0 -} - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - show_help - ;; - -o|--open) - OPEN_DOCS=true - shift - ;; - --pdf) - GENERATE_PDF=true - shift - ;; - -c|--clean) - CLEAN_ONLY=true - shift - ;; - *) - log_error "Unknown option: $1" - exit 1 - ;; - esac -done - -check_dependencies() { - if ! command -v doxygen &> /dev/null; then - log_error "doxygen not found. Install with: sudo apt install doxygen" - exit 1 - fi - - if ! command -v dot &> /dev/null; then - log_warn "graphviz not found. Diagrams will be disabled." - log_info "Install with: sudo apt install graphviz" - fi - - if [[ "$GENERATE_PDF" == true ]] && ! command -v pdflatex &> /dev/null; then - log_error "pdflatex not found. Install with: sudo apt install texlive-latex-base" - exit 1 - fi -} - -clean_docs() { - log_info "Cleaning generated documentation..." - rm -rf "$OUTPUT_DIR" - log_ok "Documentation cleaned" -} - -generate_doxyfile() { - log_info "Generating Doxyfile..." - - cat > "$DOXYFILE" << 'DOXYFILE_CONTENT' -# Doxyfile for QGroundControl - -PROJECT_NAME = "QGroundControl" -PROJECT_BRIEF = "Ground Control Station for MAVLink Drones" -PROJECT_NUMBER = -OUTPUT_DIRECTORY = docs/api - -# Input -INPUT = src -INPUT_ENCODING = UTF-8 -FILE_PATTERNS = *.h *.cc *.cpp *.hpp *.qml *.md -RECURSIVE = YES -EXCLUDE_PATTERNS = */test/* */libs/* */_deps/* */build/* - -# Output formats -GENERATE_HTML = YES -HTML_OUTPUT = html -GENERATE_LATEX = NO -GENERATE_XML = NO - -# HTML settings -HTML_COLORSTYLE_HUE = 220 -HTML_COLORSTYLE_SAT = 100 -HTML_COLORSTYLE_GAMMA = 80 -HTML_DYNAMIC_SECTIONS = YES -DISABLE_INDEX = NO -GENERATE_TREEVIEW = YES -FULL_SIDEBAR = YES - -# Extraction settings -EXTRACT_ALL = YES -EXTRACT_PRIVATE = NO -EXTRACT_STATIC = YES -EXTRACT_LOCAL_CLASSES = YES - -# Source browser -SOURCE_BROWSER = YES -INLINE_SOURCES = NO -STRIP_CODE_COMMENTS = YES -REFERENCED_BY_RELATION = YES -REFERENCES_RELATION = YES - -# Diagrams (requires graphviz) -HAVE_DOT = YES -DOT_NUM_THREADS = 0 -CLASS_DIAGRAMS = YES -COLLABORATION_GRAPH = YES -GROUP_GRAPHS = YES -INCLUDE_GRAPH = YES -INCLUDED_BY_GRAPH = YES -CALL_GRAPH = NO -CALLER_GRAPH = NO -GRAPHICAL_HIERARCHY = YES -DIRECTORY_GRAPH = YES -DOT_IMAGE_FORMAT = svg -INTERACTIVE_SVG = YES - -# Preprocessing -ENABLE_PREPROCESSING = YES -MACRO_EXPANSION = YES -EXPAND_ONLY_PREDEF = NO -PREDEFINED = Q_OBJECT Q_PROPERTY Q_INVOKABLE Q_SIGNAL Q_SLOT - -# Warnings -QUIET = NO -WARNINGS = YES -WARN_IF_UNDOCUMENTED = NO -WARN_IF_DOC_ERROR = YES - -# Other -ALPHABETICAL_INDEX = YES -GENERATE_TODOLIST = YES -GENERATE_BUGLIST = YES -SHOW_USED_FILES = YES -SHOW_FILES = YES -SHOW_NAMESPACES = YES -DOXYFILE_CONTENT - - log_ok "Doxyfile generated" -} - -generate_docs() { - log_info "Generating documentation..." - - mkdir -p "$OUTPUT_DIR" - - # Generate Doxyfile if it doesn't exist - if [[ ! -f "$DOXYFILE" ]]; then - generate_doxyfile - fi - - # Enable LaTeX if PDF requested - if [[ "$GENERATE_PDF" == true ]]; then - sed -i 's/GENERATE_LATEX.*= NO/GENERATE_LATEX = YES/' "$DOXYFILE" - fi - - # Run doxygen - cd "$REPO_ROOT" - doxygen "$DOXYFILE" - - log_ok "Documentation generated: $OUTPUT_DIR/html/index.html" - - # Generate PDF if requested - if [[ "$GENERATE_PDF" == true ]]; then - log_info "Generating PDF..." - cd "$OUTPUT_DIR/latex" - make pdf - log_ok "PDF generated: $OUTPUT_DIR/latex/refman.pdf" - fi -} - -open_docs() { - local index="$OUTPUT_DIR/html/index.html" - if [[ ! -f "$index" ]]; then - log_error "Documentation not found. Generate first." - exit 1 - fi - - log_info "Opening documentation..." - - if command -v xdg-open &> /dev/null; then - xdg-open "$index" - elif command -v open &> /dev/null; then - open "$index" - else - log_warn "Could not open browser. Docs at: $index" - fi -} - -# Main -cd "$REPO_ROOT" - -if [[ "$CLEAN_ONLY" == true ]]; then - clean_docs - exit 0 -fi - -check_dependencies -generate_docs - -if [[ "$OPEN_DOCS" == true ]]; then - open_docs -fi diff --git a/tools/generate_docs.py b/tools/generate_docs.py new file mode 100644 index 000000000000..b15aafeba7c7 --- /dev/null +++ b/tools/generate_docs.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Generate API documentation using Doxygen.""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +from _bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.logging import log_error, log_info, log_ok, log_warn + +DEFAULT_DOXYFILE = """# Doxyfile for QGroundControl + +PROJECT_NAME = "QGroundControl" +PROJECT_BRIEF = "Ground Control Station for MAVLink Drones" +OUTPUT_DIRECTORY = docs/api +INPUT = src +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.h *.cc *.cpp *.hpp *.qml *.md +RECURSIVE = YES +EXCLUDE_PATTERNS = */test/* */libs/* */_deps/* */build/* +GENERATE_HTML = YES +HTML_OUTPUT = html +GENERATE_LATEX = NO +GENERATE_XML = NO +HAVE_DOT = YES +QUIET = NO +WARNINGS = YES +""" + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Generate QGroundControl API documentation.") + parser.add_argument("-o", "--open", dest="open_docs", action="store_true", help="Open docs after generating") + parser.add_argument("--pdf", action="store_true", help="Generate PDF output") + parser.add_argument("-c", "--clean", action="store_true", help="Clean generated docs") + parser.add_argument("--output-dir", default="docs/api", help="Output directory (default: docs/api)") + parser.add_argument("--doxyfile", default="Doxyfile", help="Path to Doxyfile (default: Doxyfile)") + return parser.parse_args(argv) + + +def build_doxyfile_text(base_text: str, output_dir: Path, *, generate_pdf: bool) -> str: + """Return a Doxygen config with safe per-run overrides appended.""" + latex_value = "YES" if generate_pdf else "NO" + override = ( + "\n" + f'OUTPUT_DIRECTORY = "{output_dir.as_posix()}"\n' + f"GENERATE_LATEX = {latex_value}\n" + ) + return base_text.rstrip() + override + + +def check_dependencies(*, generate_pdf: bool) -> None: + """Ensure the required tooling is installed.""" + if shutil.which("doxygen") is None: + raise RuntimeError("doxygen not found. Install with: sudo apt install doxygen") + if shutil.which("dot") is None: + log_warn("graphviz not found. Diagrams will be disabled.") + if generate_pdf and shutil.which("pdflatex") is None: + raise RuntimeError("pdflatex not found. Install with: sudo apt install texlive-latex-base") + + +def clean_docs(output_dir: Path) -> None: + """Remove generated documentation.""" + log_info("Cleaning generated documentation...") + shutil.rmtree(output_dir, ignore_errors=True) + log_ok("Documentation cleaned") + + +def load_base_doxyfile(doxyfile_path: Path) -> str: + """Load the repo Doxyfile or fall back to a generated default.""" + if doxyfile_path.exists(): + return doxyfile_path.read_text(encoding="utf-8") + log_warn(f"{doxyfile_path.name} not found. Using generated default configuration.") + return DEFAULT_DOXYFILE + + +def generate_docs(repo_root: Path, output_dir: Path, doxyfile_path: Path, *, generate_pdf: bool) -> None: + """Generate HTML docs and optionally PDF docs without mutating tracked files.""" + log_info("Generating documentation...") + output_dir.mkdir(parents=True, exist_ok=True) + + base_text = load_base_doxyfile(doxyfile_path) + effective_text = build_doxyfile_text(base_text, output_dir, generate_pdf=generate_pdf) + + with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".doxyfile", delete=False) as handle: + handle.write(effective_text) + temp_doxyfile = Path(handle.name) + + try: + subprocess.run(["doxygen", str(temp_doxyfile)], cwd=repo_root, check=True) + finally: + temp_doxyfile.unlink(missing_ok=True) + + log_ok(f"Documentation generated: {output_dir / 'html' / 'index.html'}") + + if generate_pdf: + latex_dir = output_dir / "latex" + log_info("Generating PDF...") + subprocess.run(["make", "pdf"], cwd=latex_dir, check=True) + log_ok(f"PDF generated: {latex_dir / 'refman.pdf'}") + + +def open_docs(output_dir: Path) -> None: + """Open the generated HTML documentation.""" + index = output_dir / "html" / "index.html" + if not index.exists(): + raise FileNotFoundError(f"Documentation not found: {index}") + + log_info("Opening documentation...") + opener = shutil.which("xdg-open") or shutil.which("open") + if opener is None: + raise RuntimeError(f"Could not find a browser opener. Docs at: {index}") + subprocess.run([opener, str(index)], check=False) + + +def main(argv: list[str] | None = None) -> int: + """Run the requested documentation workflow.""" + args = parse_args(argv) + repo_root = Path(__file__).resolve().parent.parent + output_dir = Path(args.output_dir) + doxyfile_path = Path(args.doxyfile) + if not output_dir.is_absolute(): + output_dir = repo_root / output_dir + if not doxyfile_path.is_absolute(): + doxyfile_path = repo_root / doxyfile_path + + try: + if args.clean: + clean_docs(output_dir) + return 0 + + check_dependencies(generate_pdf=args.pdf) + generate_docs(repo_root, output_dir, doxyfile_path, generate_pdf=args.pdf) + if args.open_docs: + open_docs(output_dir) + return 0 + except (RuntimeError, FileNotFoundError, subprocess.CalledProcessError) as exc: + log_error(str(exc)) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/param-docs.py b/tools/param-docs.py index be95aecabf9f..d294bc401b74 100755 --- a/tools/param-docs.py +++ b/tools/param-docs.py @@ -285,7 +285,7 @@ def generate_json_output(params: list[Parameter]) -> str: return json.dumps(output, indent=2) -def main(): +def main() -> int: parser = argparse.ArgumentParser( description="Generate parameter documentation from FactMetaData JSON files", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/tools/pre-commit.sh b/tools/pre-commit.sh deleted file mode 100755 index 04a0b906e151..000000000000 --- a/tools/pre-commit.sh +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env bash -# -# Run pre-commit checks with formatted output -# -# Usage: -# ./tools/pre-commit.sh # Run on all files -# ./tools/pre-commit.sh --changed # Run only on changed files (vs master) -# ./tools/pre-commit.sh --install # Install pre-commit hooks -# ./tools/pre-commit.sh --update # Update hook versions -# ./tools/pre-commit.sh --ci # CI mode (machine-readable output) -# -# Environment: -# PRE_COMMIT_OUTPUT - File to write output (default: stdout) -# GITHUB_STEP_SUMMARY - GitHub Actions step summary file -# - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } -log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } - -# Defaults -MODE="all" -CI_MODE=false -OUTPUT_FILE="" - -show_help() { - head -13 "$0" | tail -11 - exit 0 -} - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - show_help - ;; - -c|--changed) - MODE="changed" - shift - ;; - -i|--install) - MODE="install" - shift - ;; - -u|--update) - MODE="update" - shift - ;; - --ci) - CI_MODE=true - shift - ;; - -o|--output) - OUTPUT_FILE="$2" - shift 2 - ;; - *) - log_error "Unknown option: $1" - show_help - ;; - esac -done - -cd "$REPO_ROOT" - -# Check for pre-commit -if ! command -v pre-commit &> /dev/null; then - log_error "pre-commit not found" - log_info "Install with: pip install pre-commit" - log_info "Or run: ./tools/pre-commit.sh --install" - exit 1 -fi - -# Handle install/update modes -if [[ "$MODE" == "install" ]]; then - log_info "Installing pre-commit and hooks..." - pip install pre-commit - pre-commit install - pre-commit install --hook-type commit-msg - log_ok "Pre-commit hooks installed" - exit 0 -fi - -if [[ "$MODE" == "update" ]]; then - log_info "Updating pre-commit hooks..." - pre-commit autoupdate - log_ok "Hooks updated. Review changes in .pre-commit-config.yaml" - exit 0 -fi - -# Build pre-commit arguments -ARGS=( - "--show-diff-on-failure" - "--color=always" -) - -if [[ "$MODE" == "changed" ]]; then - # Check if we can compare against master - # SECURITY: Branch name "master" is hardcoded to prevent command injection. - # If making this dynamic, validate: [[ "$branch" =~ ^[a-zA-Z0-9/_-]+$ ]] - if git rev-parse --verify master &>/dev/null || git rev-parse --verify origin/master &>/dev/null; then - log_info "Running on files changed vs master..." - ARGS+=("--from-ref" "master" "--to-ref" "HEAD") - else - log_warn "master branch not available, running on all files" - ARGS+=("--all-files") - fi -else - ARGS+=("--all-files") -fi - -# Create temp file for output capture -TEMP_OUTPUT=$(mktemp) -trap 'rm -f "$TEMP_OUTPUT"' EXIT - -log_info "Running pre-commit checks..." -echo "" - -# Run pre-commit and capture output -set +e -pre-commit run "${ARGS[@]}" 2>&1 | tee "$TEMP_OUTPUT" -EXIT_CODE=${PIPESTATUS[0]} -set -e - -echo "" - -# Parse results - single awk pass for efficiency -read -r PASSED FAILED SKIPPED < <(awk '/Passed/{p++} /Failed/{f++} /Skipped/{s++} END{print p+0, f+0, s+0}' "$TEMP_OUTPUT" 2>/dev/null || echo "0 0 0") - -# Summary -if [[ $EXIT_CODE -eq 0 ]]; then - log_ok "All checks passed ($PASSED passed)" -else - log_error "Some checks failed ($PASSED passed, $FAILED failed)" -fi - -# Write to output file if specified -if [[ -n "$OUTPUT_FILE" ]] || [[ -n "${PRE_COMMIT_OUTPUT:-}" ]]; then - OUTPUT_FILE="${OUTPUT_FILE:-$PRE_COMMIT_OUTPUT}" - cp "$TEMP_OUTPUT" "$OUTPUT_FILE" - log_info "Output written to: $OUTPUT_FILE" -fi - -# CI mode: write structured output -if [[ "$CI_MODE" == true ]]; then - # Write to GITHUB_OUTPUT if available - if [[ -n "${GITHUB_OUTPUT:-}" ]]; then - { - echo "exit_code=$EXIT_CODE" - echo "passed=$PASSED" - echo "failed=$FAILED" - echo "skipped=$SKIPPED" - } >> "$GITHUB_OUTPUT" - - # Write summary to GITHUB_OUTPUT (multiline with unique delimiter) - DELIMITER="PRECOMMIT_SUMMARY_$(date +%s)" - { - echo "summary<<${DELIMITER}" - grep -E '\.\.\.\.\.\.\.\.\.\.' "$TEMP_OUTPUT" 2>/dev/null | head -40 | sed 's/\x1b\[[0-9;]*m//g' || echo "No results" - echo "${DELIMITER}" - } >> "$GITHUB_OUTPUT" - fi - - # Write to GITHUB_STEP_SUMMARY if available - if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then - { - echo "## Pre-commit Results" - echo "" - if [[ $EXIT_CODE -eq 0 ]]; then - echo "**All checks passed**" - else - echo "**Some checks failed**" - fi - echo "" - echo "| Status | Count |" - echo "|--------|-------|" - echo "| Passed | $PASSED |" - echo "| Failed | $FAILED |" - if [[ $SKIPPED -gt 0 ]]; then - echo "| Skipped | $SKIPPED |" - fi - echo "" - echo "
    " - echo "Hook Results" - echo "" - echo '```' - grep -E '\.\.\.\.\.\.\.\.\.\.' "$TEMP_OUTPUT" 2>/dev/null | head -40 | sed 's/\x1b\[[0-9;]*m//g' || echo "No results" - echo '```' - echo "
    " - - # Show modified files if any - if ! git diff --quiet 2>/dev/null; then - echo "" - echo "
    " - echo "Files Modified by Hooks" - echo "" - echo '```' - git diff --stat 2>/dev/null || true - echo '```' - echo "
    " - fi - } >> "$GITHUB_STEP_SUMMARY" - fi -fi - -# Show fix instructions on failure -if [[ $EXIT_CODE -ne 0 ]]; then - echo "" - log_info "To fix issues locally:" - echo " 1. Run: pre-commit run --all-files" - echo " 2. Review and stage changes: git add -u" - echo " 3. Amend your commit: git commit --amend --no-edit" -fi - -exit $EXIT_CODE diff --git a/tools/pre_commit.py b/tools/pre_commit.py new file mode 100644 index 000000000000..b4a583609186 --- /dev/null +++ b/tools/pre_commit.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Run pre-commit checks with CI-friendly summaries.""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +from _bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.logging import log_error, log_info, log_ok, log_warn + +HOOK_RESULT_RE = re.compile(r"\b(Passed|Failed|Skipped)\b") +ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run pre-commit checks.") + parser.add_argument("-c", "--changed", action="store_true", help="Run only files changed vs master") + parser.add_argument("-i", "--install", action="store_true", help="Install pre-commit hooks") + parser.add_argument("-u", "--update", action="store_true", help="Update hook versions") + parser.add_argument("--ci", action="store_true", help="Enable GitHub Actions outputs") + parser.add_argument("-o", "--output", default="", help="Write raw output to file") + return parser.parse_args(argv) + + +def repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def ensure_precommit_available() -> bool: + return shutil.which("pre-commit") is not None + + +def strip_ansi(value: str) -> str: + return ANSI_ESCAPE_RE.sub("", value) + + +def summarize_output(output: str) -> tuple[int, int, int]: + passed = failed = skipped = 0 + for line in output.splitlines(): + match = HOOK_RESULT_RE.search(line) + if not match: + continue + state = match.group(1) + if state == "Passed": + passed += 1 + elif state == "Failed": + failed += 1 + elif state == "Skipped": + skipped += 1 + return passed, failed, skipped + + +def extract_hook_lines(output: str, *, limit: int = 40) -> list[str]: + lines = [strip_ansi(line) for line in output.splitlines() if ".........." in line] + return lines[:limit] or ["No results"] + + +def git_has_master_ref() -> bool: + commands = [ + ["git", "rev-parse", "--verify", "master"], + ["git", "rev-parse", "--verify", "origin/master"], + ] + return any(subprocess.run(cmd, capture_output=True, check=False).returncode == 0 for cmd in commands) + + +def build_precommit_args(args: argparse.Namespace) -> list[str]: + result = ["pre-commit", "run", "--show-diff-on-failure", "--color=always"] + if args.changed: + if git_has_master_ref(): + log_info("Running on files changed vs master...") + result.extend(["--from-ref", "master", "--to-ref", "HEAD"]) + else: + log_warn("master branch not available, running on all files") + result.append("--all-files") + else: + result.append("--all-files") + return result + + +def run_command(cmd: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run(cmd, capture_output=True, text=True, check=False) + + +def write_github_output(exit_code: int, passed: int, failed: int, skipped: int, summary_lines: list[str]) -> None: + github_output = os.environ.get("GITHUB_OUTPUT") + if not github_output: + return + delimiter = f"PRECOMMIT_SUMMARY_{os.getpid()}" + with open(github_output, "a", encoding="utf-8") as handle: + handle.write(f"exit_code={exit_code}\n") + handle.write(f"passed={passed}\n") + handle.write(f"failed={failed}\n") + handle.write(f"skipped={skipped}\n") + handle.write(f"summary<<{delimiter}\n") + handle.write("\n".join(summary_lines) + "\n") + handle.write(f"{delimiter}\n") + + +def write_step_summary(exit_code: int, passed: int, failed: int, skipped: int, output: str) -> None: + step_summary = os.environ.get("GITHUB_STEP_SUMMARY") + if not step_summary: + return + with open(step_summary, "a", encoding="utf-8") as handle: + handle.write("## Pre-commit Results\n\n") + handle.write("**All checks passed**\n\n" if exit_code == 0 else "**Some checks failed**\n\n") + handle.write("| Status | Count |\n|--------|-------|\n") + handle.write(f"| Passed | {passed} |\n") + handle.write(f"| Failed | {failed} |\n") + if skipped > 0: + handle.write(f"| Skipped | {skipped} |\n") + handle.write("\n
    \nHook Results\n\n```\n") + handle.write("\n".join(extract_hook_lines(output)) + "\n") + handle.write("```\n
    \n") + + diff = subprocess.run(["git", "diff", "--stat"], capture_output=True, text=True, check=False) + if diff.stdout.strip(): + handle.write("\n
    \nFiles Modified by Hooks\n\n```\n") + handle.write(diff.stdout) + handle.write("```\n
    \n") + + +def handle_install() -> int: + log_info("Installing pre-commit and hooks...") + subprocess.run([sys.executable, "-m", "pip", "install", "pre-commit"], check=True) + subprocess.run(["pre-commit", "install"], check=True) + subprocess.run(["pre-commit", "install", "--hook-type", "commit-msg"], check=True) + log_ok("Pre-commit hooks installed") + return 0 + + +def handle_update() -> int: + log_info("Updating pre-commit hooks...") + subprocess.run(["pre-commit", "autoupdate"], check=True) + log_ok("Hooks updated. Review changes in .pre-commit-config.yaml") + return 0 + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + os.chdir(repo_root()) + + try: + if args.install: + return handle_install() + if args.update: + if not ensure_precommit_available(): + log_error("pre-commit not found") + return 1 + return handle_update() + + if not ensure_precommit_available(): + log_error("pre-commit not found") + log_info("Install with: pip install pre-commit") + log_info("Or run: python3 ./tools/pre_commit.py --install") + return 1 + + command = build_precommit_args(args) + log_info("Running pre-commit checks...") + print() + result = run_command(command) + if result.stdout: + print(result.stdout, end="") + if result.stderr: + print(result.stderr, end="", file=sys.stderr) + print() + + passed, failed, skipped = summarize_output(result.stdout + result.stderr) + if args.output or os.environ.get("PRE_COMMIT_OUTPUT"): + output_path = Path(args.output or os.environ["PRE_COMMIT_OUTPUT"]) + output_path.write_text(result.stdout + result.stderr, encoding="utf-8") + log_info(f"Output written to: {output_path}") + + if result.returncode == 0: + log_ok(f"All checks passed ({passed} passed)") + else: + log_error(f"Some checks failed ({passed} passed, {failed} failed)") + + if args.ci: + summary_lines = extract_hook_lines(result.stdout + result.stderr) + write_github_output(result.returncode, passed, failed, skipped, summary_lines) + write_step_summary(result.returncode, passed, failed, skipped, result.stdout + result.stderr) + + if result.returncode != 0: + print() + log_info("To fix issues locally:") + print(" 1. Run: pre-commit run --all-files") + print(" 2. Review and stage changes: git add -u") + print(" 3. Amend your commit: git commit --amend --no-edit") + + return result.returncode + except subprocess.CalledProcessError as exc: + log_error(str(exc)) + return exc.returncode or 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/pyproject.toml b/tools/pyproject.toml new file mode 100644 index 000000000000..38ff5dac78bd --- /dev/null +++ b/tools/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "qgc-tools" +version = "0.0.0" +requires-python = ">=3.12" + +[project.optional-dependencies] +scripts = [ + "defusedxml>=0.7.1", +] +precommit = [ + "pre-commit", +] +test = [ + "pytest", + "jinja2", + "pyyaml", +] +ci = [ + "pre-commit", + "meson", + "ninja", +] +qt = [ + "aqtinstall", +] +coverage = [ + "gcovr", +] +dev = [ + "jinja2", + "pyyaml", + "pymavlink", +] +lsp = [ + "pygls", + "lsprotocol", +] + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] diff --git a/tools/release.sh b/tools/release.sh index 5bcd86a515c6..8f7bad1e337c 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -17,17 +17,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } -log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } +source "$REPO_ROOT/tools/common/shell-utils.sh" DRY_RUN=true INSTALL_DEPS=false diff --git a/tools/setup/build-gstreamer.py b/tools/setup/build-gstreamer.py index 250147d1a539..bfec09dcea6c 100755 --- a/tools/setup/build-gstreamer.py +++ b/tools/setup/build-gstreamer.py @@ -28,7 +28,6 @@ from __future__ import annotations import argparse -import json import os import platform import shutil @@ -39,32 +38,28 @@ from dataclasses import dataclass, field from pathlib import Path +try: + from .setup_bootstrap import ensure_setup_imports +except ImportError: + setup_dir = Path(__file__).resolve().parent + if str(setup_dir) not in sys.path: + sys.path.insert(0, str(setup_dir)) + from setup_bootstrap import ensure_setup_imports + +ensure_setup_imports() + +from common.logging import log_info, log_ok, log_warn, log_error +from common.build_config import get_build_config_value +from common.gh_actions import write_github_output + # ============================================================================ -# Shared Utilities +# Build Utilities # ============================================================================ MESON_VERSION = "1.10.1" NINJA_VERSION = "1.13.0" -class Colors: - RED = '\033[0;31m' - GREEN = '\033[0;32m' - YELLOW = '\033[1;33m' - BLUE = '\033[0;34m' - NC = '\033[0m' - -def log_info(msg: str) -> None: - print(f"{Colors.BLUE}[INFO]{Colors.NC} {msg}") - -def log_ok(msg: str) -> None: - print(f"{Colors.GREEN}[OK]{Colors.NC} {msg}") - -def log_warn(msg: str) -> None: - print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}", file=sys.stderr) - -def log_error(msg: str) -> None: - print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}", file=sys.stderr) def run_cmd(cmd: list, cwd: Path | None = None, check: bool = True, env: dict | None = None) -> subprocess.CompletedProcess: @@ -100,29 +95,6 @@ def detect_host_arch() -> str: return 'armv7' return machine -def read_config(key: str, default: str = '') -> str: - """Read a value from build-config.json.""" - script_dir = Path(__file__).parent - config_file = script_dir.parent.parent / '.github' / 'build-config.json' - - if not config_file.exists(): - return default - - try: - with open(config_file) as f: - config = json.load(f) - return config.get(key, default) - except Exception: - return default - -def write_github_output(outputs: dict) -> None: - """Write outputs for GitHub Actions.""" - github_output = os.environ.get('GITHUB_OUTPUT') - if github_output: - with open(github_output, 'a') as f: - for key, value in outputs.items(): - f.write(f"{key}={value}\n") - # ============================================================================ # Plugin Configurations @@ -632,7 +604,11 @@ def main() -> int: args = parse_args() # Resolve defaults - version = args.version or read_config('gstreamer_version', '1.24.10') + version = args.version or get_build_config_value( + 'gstreamer_version', + '1.24.10', + start=Path(__file__).resolve(), + ) arch = args.arch or get_default_arch(args.platform) prefix = Path(args.prefix) if args.prefix else None work_dir = Path(args.work_dir) diff --git a/tools/setup/download_artifacts.py b/tools/setup/download_artifacts.py index 1a73d5b44765..aca768b3fd73 100755 --- a/tools/setup/download_artifacts.py +++ b/tools/setup/download_artifacts.py @@ -17,19 +17,23 @@ import argparse import concurrent.futures -from datetime import datetime import json -import subprocess import sys from pathlib import Path from typing import Any -REPO_ROOT = Path(__file__).resolve().parents[2] -COMMON_DIR = REPO_ROOT / "tools" / "common" -if str(COMMON_DIR) not in sys.path: - sys.path.insert(0, str(COMMON_DIR)) +try: + from .setup_bootstrap import ensure_setup_imports +except ImportError: + setup_dir = Path(__file__).resolve().parent + if str(setup_dir) not in sys.path: + sys.path.insert(0, str(setup_dir)) + from setup_bootstrap import ensure_setup_imports -import gh_actions as _gh_actions +ensure_setup_imports() + +from common.gh_actions import gh, list_workflow_runs_for_sha, list_run_artifacts +from common.github_runs import group_runs_by_name, select_latest_runs_by_name ARTIFACT_EXTENSIONS = ( @@ -44,52 +48,20 @@ ) ARTIFACT_EXTENSION_SET = frozenset(ARTIFACT_EXTENSIONS) - -def gh(*args: str, check: bool = True) -> subprocess.CompletedProcess: - """Run a gh CLI command and return the result.""" - return _gh_actions.gh(*args, check=check) - - -def _parse_created_at(created_at: Any) -> datetime | None: - value = str(created_at).strip() - if not value: - return None - if value.endswith("Z"): - value = f"{value[:-1]}+00:00" - try: - return datetime.fromisoformat(value) - except ValueError: - return None - - -def _is_newer_run(candidate: dict[str, Any], existing: dict[str, Any]) -> bool: - candidate_created_at = str(candidate.get("created_at", "")) - existing_created_at = str(existing.get("created_at", "")) - candidate_dt = _parse_created_at(candidate_created_at) - existing_dt = _parse_created_at(existing_created_at) - if candidate_dt is not None and existing_dt is not None: - return candidate_dt > existing_dt - return candidate_created_at > existing_created_at - - def select_latest_successful_runs( runs: list[dict[str, Any]], workflows: list[str], + *, + event: str = "", ) -> list[dict[str, Any]]: """Return latest completed/successful run per workflow name.""" - workflow_set = set(workflows) - latest_by_workflow: dict[str, dict[str, Any]] = {} - - for run in runs: - name = str(run.get("name", "")) - if name not in workflow_set: - continue - if run.get("status") != "completed" or run.get("conclusion") != "success": - continue - existing = latest_by_workflow.get(name) - if existing is None or _is_newer_run(run, existing): - latest_by_workflow[name] = run - + latest_by_workflow = select_latest_runs_by_name( + runs, + set(workflows), + event=event, + status="completed", + conclusion="success", + ) order = {name: idx for idx, name in enumerate(workflows)} return sorted( latest_by_workflow.values(), @@ -100,40 +72,28 @@ def select_latest_successful_runs( def group_successful_runs_by_workflow( runs: list[dict[str, Any]], workflows: list[str], + *, + event: str = "", ) -> dict[str, list[dict[str, Any]]]: """Group successful runs by workflow name, newest first.""" - workflow_set = set(workflows) - grouped: dict[str, list[dict[str, Any]]] = {name: [] for name in workflows} - - for run in runs: - name = str(run.get("name", "")) - if name not in workflow_set: - continue - if run.get("status") != "completed" or run.get("conclusion") != "success": - continue - grouped[name].append(run) - - for workflow_name in grouped: - grouped[workflow_name].sort( - key=lambda run: ( - _parse_created_at(run.get("created_at")) is not None, - str(run.get("created_at", "")), - ), - reverse=True, - ) - - return grouped + return group_runs_by_name( + runs, + workflows, + event=event, + status="completed", + conclusion="success", + ) -def get_workflow_runs(repo: str, head_sha: str, workflows: list[str]) -> list[dict]: +def get_workflow_runs(repo: str, head_sha: str, workflows: list[str], *, event: str = "") -> list[dict]: """Return completed successful runs for the given workflows and commit.""" - all_runs = _gh_actions.list_workflow_runs_for_sha(repo, head_sha) - return select_latest_successful_runs(all_runs, workflows) + all_runs = list_workflow_runs_for_sha(repo, head_sha) + return select_latest_successful_runs(all_runs, workflows, event=event) def select_artifact_names_for_run(repo: str, run_id: int, prefixes: list[str]) -> list[str]: """Return artifact names that match configured name prefixes.""" - return select_artifact_names(_gh_actions.list_run_artifacts(repo, run_id), prefixes) + return select_artifact_names(list_run_artifacts(repo, run_id), prefixes) def select_artifact_names(artifacts: list[dict[str, Any]], prefixes: list[str]) -> list[str]: @@ -213,6 +173,12 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: default="Linux,Windows,MacOS,Android", help="Comma-separated workflow names to download from (default: Linux,Windows,MacOS,Android)", ) + parser.add_argument( + "--event", + default="", + choices=["", "push", "pull_request", "workflow_dispatch", "schedule"], + help="Optional workflow event name to filter runs by", + ) parser.add_argument( "--runs-file", default="", @@ -241,6 +207,7 @@ def main(argv: list[str] | None = None) -> int: head_sha = args.head_sha output_dir = args.output_dir workflows = [w.strip() for w in args.workflows.split(",") if w.strip()] + event = str(args.event).strip() artifact_prefixes = [p.strip() for p in args.artifact_prefixes.split(",") if p.strip()] artifact_metadata_out = Path(args.artifact_metadata_out) if args.artifact_metadata_out else None @@ -265,18 +232,18 @@ def main(argv: list[str] | None = None) -> int: return 1 all_runs = loaded_runs else: - all_runs = _gh_actions.list_workflow_runs_for_sha(repo, head_sha) + all_runs = list_workflow_runs_for_sha(repo, head_sha) preloaded_artifacts: dict[int, list[dict[str, Any]]] = {} - had_successful_runs = bool(select_latest_successful_runs(all_runs, workflows)) + had_successful_runs = bool(select_latest_successful_runs(all_runs, workflows, event=event)) if artifact_prefixes: runs = [] - grouped_runs = group_successful_runs_by_workflow(all_runs, workflows) + grouped_runs = group_successful_runs_by_workflow(all_runs, workflows, event=event) for workflow_name in workflows: candidates = grouped_runs.get(workflow_name, []) selected_run: dict[str, Any] | None = None for candidate in candidates: run_id = int(candidate["id"]) - artifacts = _gh_actions.list_run_artifacts(repo, run_id) + artifacts = list_run_artifacts(repo, run_id) preloaded_artifacts[run_id] = artifacts if select_artifact_names(artifacts, artifact_prefixes): selected_run = candidate @@ -284,7 +251,7 @@ def main(argv: list[str] | None = None) -> int: if selected_run is not None: runs.append(selected_run) else: - runs = select_latest_successful_runs(all_runs, workflows) + runs = select_latest_successful_runs(all_runs, workflows, event=event) if not runs: if artifact_prefixes and had_successful_runs: @@ -317,7 +284,7 @@ def main(argv: list[str] | None = None) -> int: if artifact_prefixes or artifact_metadata_out is not None: artifacts = preloaded_artifacts.get(run_id) if artifacts is None: - artifacts = _gh_actions.list_run_artifacts(repo, run_id) + artifacts = list_run_artifacts(repo, run_id) if artifact_prefixes: names = select_artifact_names(artifacts, artifact_prefixes) run_artifact_map[run_id] = names if names else None diff --git a/tools/setup/install_dependencies.py b/tools/setup/install_dependencies.py index e72466407b41..cf769f222e05 100644 --- a/tools/setup/install_dependencies.py +++ b/tools/setup/install_dependencies.py @@ -16,6 +16,7 @@ import argparse import json import os +import re import shlex import shutil import subprocess @@ -25,6 +26,19 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path +try: + from .setup_bootstrap import ensure_setup_imports +except ImportError: + setup_dir = Path(__file__).resolve().parent + if str(setup_dir) not in sys.path: + sys.path.insert(0, str(setup_dir)) + from setup_bootstrap import ensure_setup_imports + +ensure_setup_imports() + +from common.file_traversal import find_repo_root +from common.build_config import load_build_config as load_shared_build_config + # Package categories for Debian/Ubuntu DEBIAN_PACKAGES: dict[str, list[str]] = { "core": [ @@ -155,28 +169,19 @@ "-o", "DPkg::Lock::Timeout=300", "-o", "Acquire::Retries=3", ] - - -def get_repo_root() -> Path: - """Find repository root directory.""" - current = Path(__file__).resolve() - for parent in [current] + list(current.parents): - if (parent / ".git").exists(): - return parent - return Path.cwd() +PACKAGE_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9.+-]*$") def load_build_config() -> dict: """Load build configuration from .github/build-config.json.""" - config_path = get_repo_root() / ".github" / "build-config.json" - if config_path.exists(): - try: - with open(config_path) as f: - return json.load(f) - except json.JSONDecodeError as e: - print(f"Error: invalid JSON in {config_path}: {e}", file=sys.stderr) - return {} - return {} + config_path = find_repo_root() / ".github" / "build-config.json" + if not config_path.exists(): + return {} + try: + return load_shared_build_config(config_path) + except (json.JSONDecodeError, OSError, ValueError) as e: + print(f"Error: invalid JSON in {config_path}: {e}", file=sys.stderr) + return {} def get_config_value(key: str) -> str | None: @@ -285,6 +290,21 @@ def check_apt_package_available(package: str) -> bool: return result.returncode == 0 +def get_available_debian_packages(category: str) -> list[str]: + """Return packages in *category* that exist in apt metadata.""" + return [pkg for pkg in get_debian_packages(category) if check_apt_package_available(pkg)] + + +def validate_extra_packages(packages: list[str]) -> list[str]: + """Validate extra package names passed from CI inputs.""" + validated: list[str] = [] + for package in packages: + if not PACKAGE_NAME_RE.match(package): + raise ValueError(f"Invalid package name '{package}'") + validated.append(package) + return validated + + def get_gstreamer_macos_urls(version: str) -> tuple[str, str]: """Get GStreamer download URLs for macOS.""" base_url = f"https://gstreamer.freedesktop.org/data/pkg/osx/{version}" @@ -746,6 +766,11 @@ def parse_args(args: list[str] | None = None) -> argparse.Namespace: action="store_true", help="Print space-separated package list (machine-readable, for CI caching)", ) + parser.add_argument( + "--print-available-packages", + action="store_true", + help="Print available apt packages in the selected Debian category", + ) parser.add_argument( "--category", help="Install only specific category (Debian only)", @@ -769,6 +794,12 @@ def parse_args(args: list[str] | None = None) -> argparse.Namespace: action="store_true", help="Install Vulkan SDK (Windows only)", ) + parser.add_argument( + "--validate-extra-packages", + nargs="*", + default=None, + help="Validate extra apt package names and print them back", + ) return parser.parse_args(args) @@ -787,6 +818,24 @@ def main() -> int: print_packages(platform or "debian", args.category) return 0 + if args.print_available_packages: + if (platform or "debian") != "debian": + print("Error: --print-available-packages is only supported for debian", file=sys.stderr) + return 1 + if not args.category: + print("Error: --category is required with --print-available-packages", file=sys.stderr) + return 1 + print(" ".join(get_available_debian_packages(args.category))) + return 0 + + if args.validate_extra_packages is not None: + try: + print(" ".join(validate_extra_packages(args.validate_extra_packages))) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + return 0 + if platform is None: print("Error: Could not detect platform", file=sys.stderr) print("Use --platform to specify: debian, macos, windows", file=sys.stderr) diff --git a/tools/setup/install_python.py b/tools/setup/install_python.py index ce2e277b3ff8..e591761cbe3d 100755 --- a/tools/setup/install_python.py +++ b/tools/setup/install_python.py @@ -14,61 +14,50 @@ from __future__ import annotations import argparse +import functools +import os import shutil import subprocess import sys +import tomllib from pathlib import Path -PACKAGE_GROUPS: dict[str, list[str]] = { - "precommit": [ - "pre-commit", - ], - "test": [ - "pytest", - "jinja2", - "pyyaml", - ], - "ci": [ - "pre-commit", - "meson", - "ninja", - ], - "qt": [ - "aqtinstall", - ], - "coverage": [ - "gcovr", - ], - "dev": [ - "jinja2", - "pyyaml", - "pymavlink", - ], - "lsp": [ - "pygls", - "lsprotocol", - ], - "all": [], # Populated below -} - -# 'all' includes everything from other groups -PACKAGE_GROUPS["all"] = list( - set( - pkg - for group, packages in PACKAGE_GROUPS.items() - if group != "all" - for pkg in packages +try: + from .setup_bootstrap import ensure_setup_imports +except ImportError: + setup_dir = Path(__file__).resolve().parent + if str(setup_dir) not in sys.path: + sys.path.insert(0, str(setup_dir)) + from setup_bootstrap import ensure_setup_imports + +ensure_setup_imports() + +from common.file_traversal import find_repo_root + + +@functools.lru_cache(maxsize=1) +def load_package_groups() -> dict[str, list[str]]: + """Load dependency groups from tools/pyproject.toml.""" + pyproject_path = find_repo_root() / "tools" / "pyproject.toml" + data = tomllib.loads(pyproject_path.read_text(encoding="utf-8")) + optional = data.get("project", {}).get("optional-dependencies", {}) + if not isinstance(optional, dict): + raise ValueError("project.optional-dependencies is missing from tools/pyproject.toml") + + groups: dict[str, list[str]] = {} + for group, packages in optional.items(): + if isinstance(packages, list): + groups[group] = [str(pkg) for pkg in packages] + + groups["all"] = sorted( + { + package + for group, packages in groups.items() + if group != "all" + for package in packages + } ) -) - - -def get_repo_root() -> Path: - """Find repository root directory.""" - current = Path(__file__).resolve() - for parent in [current] + list(current.parents): - if (parent / ".git").exists(): - return parent - return Path.cwd() + return groups def has_uv() -> bool: @@ -76,9 +65,19 @@ def has_uv() -> bool: return shutil.which("uv") is not None +def get_lockfile_path() -> Path: + """Return the uv lockfile path for the tools project.""" + return find_repo_root() / "tools" / "uv.lock" + + +def get_project_path() -> Path: + """Return the tools project path.""" + return find_repo_root() / "tools" + + def get_venv_path() -> Path: """Get path to virtual environment.""" - return get_repo_root() / ".venv" + return find_repo_root() / ".venv" def get_activate_script(venv_path: Path) -> Path: @@ -132,25 +131,52 @@ def install_packages(venv_path: Path, packages: list[str]) -> None: subprocess.run(cmd, check=True) +def sync_groups_with_uv(venv_path: Path, group_spec: str) -> None: + """Sync dependency groups from the locked tools project into the repo venv.""" + lockfile_path = get_lockfile_path() + if not lockfile_path.exists(): + raise FileNotFoundError(f"uv lockfile not found at {lockfile_path}") + + groups = [group.strip() for group in group_spec.split(",") if group.strip()] + cmd = [ + "uv", + "sync", + "--project", + str(get_project_path()), + "--active", + "--no-install-project", + "--frozen", + ] + + if "all" in groups: + cmd.append("--all-extras") + else: + for group in groups: + cmd.extend(["--extra", group]) + + env = os.environ.copy() + env["VIRTUAL_ENV"] = str(venv_path) + scripts_dir = venv_path / ("Scripts" if sys.platform == "win32" else "bin") + env["PATH"] = f"{scripts_dir}{os.pathsep}{env.get('PATH', '')}" + subprocess.run(cmd, check=True, env=env) + + def list_packages(venv_path: Path) -> None: """List installed packages.""" python = get_python_executable(venv_path) - - if has_uv(): - subprocess.run(["uv", "pip", "list", "--python", str(python)]) - else: - subprocess.run([str(python), "-m", "pip", "list"]) + subprocess.run([str(python), "-m", "pip", "list"]) def get_packages_for_groups(group_spec: str) -> list[str]: """Get packages for comma-separated group specification.""" + package_groups = load_package_groups() groups = [g.strip() for g in group_spec.split(",")] packages: set[str] = set() for group in groups: - if group not in PACKAGE_GROUPS: - raise ValueError(f"Unknown group: {group}. Valid groups: {', '.join(PACKAGE_GROUPS.keys())}") - packages.update(PACKAGE_GROUPS[group]) + if group not in package_groups: + raise ValueError(f"Unknown group: {group}. Valid groups: {', '.join(package_groups.keys())}") + packages.update(package_groups[group]) return sorted(packages) @@ -206,8 +232,9 @@ def main() -> int: args = parse_args() if args.list_groups: + package_groups = load_package_groups() print("Available package groups:") - for group, packages in sorted(PACKAGE_GROUPS.items()): + for group, packages in sorted(package_groups.items()): print(f" {group}:") for pkg in sorted(packages): print(f" - {pkg}") @@ -230,10 +257,17 @@ def main() -> int: try: create_venv(venv_path) - install_packages(venv_path, packages) + if has_uv(): + print("Using uv sync (locked mode)") + sync_groups_with_uv(venv_path, args.group) + else: + install_packages(venv_path, packages) except subprocess.CalledProcessError as e: print(f"Error: Command failed with exit code {e.returncode}", file=sys.stderr) return 1 + except FileNotFoundError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 print() print("Done! Activate the environment with:") diff --git a/tools/setup/read_config.py b/tools/setup/read_config.py index 3799e817e9e9..244e658ae269 100755 --- a/tools/setup/read_config.py +++ b/tools/setup/read_config.py @@ -26,69 +26,22 @@ from pathlib import Path from typing import Any +try: + from .setup_bootstrap import ensure_setup_imports +except ImportError: + setup_dir = Path(__file__).resolve().parent + if str(setup_dir) not in sys.path: + sys.path.insert(0, str(setup_dir)) + from setup_bootstrap import ensure_setup_imports -def find_config_file() -> Path: - """Find build-config.json by searching up from script location.""" - # Check for override - if env_path := os.environ.get("CONFIG_FILE"): - return Path(env_path) - - # Check script directory first (for Docker builds where config is copied) - script_dir = Path(__file__).parent - local_config = script_dir / "build-config.json" - if local_config.exists(): - return local_config - - # Search up for .github/build-config.json - current = script_dir.resolve() - while current != current.parent: - config_path = current / ".github" / "build-config.json" - if config_path.exists(): - return config_path - current = current.parent - - raise FileNotFoundError("Could not find build-config.json") - - -def load_config(config_file: Path) -> dict[str, Any]: - """Load and parse the JSON config file.""" - with open(config_file) as f: - return json.load(f) - - -# Keys to export as environment variables (top-level simple values) -EXPORT_KEYS = [ - "qt_version", - "qt_minimum_version", - "qt_modules", - "gstreamer_minimum_version", - "gstreamer_version", - "gstreamer_macos_version", - "gstreamer_android_version", - "gstreamer_windows_version", - "xcode_version", - "xcode_ios_version", - "ndk_version", - "ndk_full_version", - "java_version", - "android_platform", - "android_min_sdk", - "android_build_tools", - "android_cmdline_tools", - "cmake_minimum_version", - "macos_deployment_target", - "ios_deployment_target", -] - - -def get_export_values(config: dict[str, Any]) -> dict[str, str]: - """Extract values to export, converting to uppercase env var names.""" - result = {} - for key in EXPORT_KEYS: - if key in config: - env_name = key.upper() - result[env_name] = str(config[key]) - return result +ensure_setup_imports() + +from common.build_config import ( + export_build_config_values, + find_build_config, + format_github_output, + load_build_config, +) def _escape_bash_value(value: str) -> str: @@ -121,23 +74,6 @@ def format_powershell_export(values: dict[str, str]) -> str: return "\n".join(lines) -def format_github_output(values: dict[str, str]) -> str: - """Format for GITHUB_OUTPUT file.""" - lines = [] - for key, value in sorted(values.items()): - # GitHub output uses lowercase keys - lines.append(f"{key.lower()}={value}") - - # Add derived values - if "QT_MODULES" in values: - ios_excluded = {"qtserialport", "qtscxml"} - modules = values["QT_MODULES"].split() - ios_modules = " ".join(m for m in modules if m not in ios_excluded) - lines.append(f"qt_modules_ios={ios_modules}") - - return "\n".join(lines) - - def main() -> int: parser = argparse.ArgumentParser( description="Read build configuration from .github/build-config.json" @@ -168,9 +104,13 @@ def main() -> int: if args.config: config_file = Path(args.config) else: - config_file = find_config_file() + script_dir = Path(__file__).parent + config_file = find_build_config( + script_dir, + extra_candidates=[script_dir / "build-config.json"], + ) - config = load_config(config_file) + config = load_build_config(config_file) except FileNotFoundError as e: print(f"Error: {e}", file=sys.stderr) return 1 @@ -195,7 +135,7 @@ def main() -> int: return 0 # Get export values - values = get_export_values(config) + values = export_build_config_values(config) # Handle --export for shell statements if args.export: diff --git a/tools/setup/resolve_gstreamer_config.py b/tools/setup/resolve_gstreamer_config.py new file mode 100644 index 000000000000..ef4dd4a8ad03 --- /dev/null +++ b/tools/setup/resolve_gstreamer_config.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Resolve GStreamer and Qt versions for GitHub Actions.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +try: + from .setup_bootstrap import ensure_setup_imports +except ImportError: + setup_dir = Path(__file__).resolve().parent + if str(setup_dir) not in sys.path: + sys.path.insert(0, str(setup_dir)) + from setup_bootstrap import ensure_setup_imports + +ensure_setup_imports() + +from common.build_config import get_build_config_value +from common.gh_actions import write_github_output + +VERSION_KEYS = { + "macos": "gstreamer_macos_version", + "windows": "gstreamer_windows_version", + "android": "gstreamer_android_version", +} + + +def resolve_version(platform_name: str, requested_version: str) -> str: + """Return explicit version override or platform-specific config version.""" + if requested_version: + return requested_version + version_key = VERSION_KEYS.get(platform_name, "gstreamer_version") + return get_build_config_value(version_key, start=Path(__file__).resolve()) + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Resolve GStreamer action config values.") + parser.add_argument("--platform", required=True, choices=["linux", "macos", "windows", "android", "ios"]) + parser.add_argument("--version", default="", help="Explicit GStreamer version override") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Resolve config values and emit GitHub outputs.""" + args = parse_args(argv) + outputs = { + "py_cmd": "python3", + "version": resolve_version(args.platform, args.version), + "qt_version": get_build_config_value("qt_version", start=Path(__file__).resolve()), + } + write_github_output(outputs) + print(f"Building GStreamer {outputs['version']} for {args.platform}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/setup/setup_bootstrap.py b/tools/setup/setup_bootstrap.py new file mode 100644 index 000000000000..97ca816dd165 --- /dev/null +++ b/tools/setup/setup_bootstrap.py @@ -0,0 +1,14 @@ +"""Bootstrap helper for entrypoints under tools/setup.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def ensure_setup_imports() -> Path: + """Ensure the parent tools directory is importable.""" + tools_dir = Path(__file__).resolve().parents[1] + if str(tools_dir) not in sys.path: + sys.path.insert(0, str(tools_dir)) + return tools_dir diff --git a/tools/simulation/mock_vehicle.py b/tools/simulation/mock_vehicle.py index d1e112d27dc6..582358c731d5 100755 --- a/tools/simulation/mock_vehicle.py +++ b/tools/simulation/mock_vehicle.py @@ -389,7 +389,7 @@ def run(self, rate=10): self.running = False -def main(): +def main() -> None: parser = argparse.ArgumentParser( description="Mock MAVLink vehicle for QGroundControl testing", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/tools/simulation/run-arducopter-sitl.sh b/tools/simulation/run-arducopter-sitl.sh index a2710d9d4a15..ad790806eab3 100755 --- a/tools/simulation/run-arducopter-sitl.sh +++ b/tools/simulation/run-arducopter-sitl.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # ArduCopter SITL Test Environment # Runs ArduCopter in Docker for testing QGC without hardware # @@ -7,7 +7,7 @@ # Options: # --with-latency Simulate 100ms round-trip latency (Herelink-like conditions) -set -e +set -euo pipefail COPTER_VERSION="Copter-4.5.6" IMAGE_NAME="ardupilot-sitl-4.5.6" @@ -36,7 +36,7 @@ DOCKER_CMD="docker run -d --name $CONTAINER_NAME -p 5760:5760" ARDUPILOT_CMD="/ardupilot/build/sitl/bin/arducopter -S --model + --speedup 1 --defaults /ardupilot/Tools/autotest/default_params/copter.parm --home 42.3898,-71.1476,14.0,270.0 --serial0 tcp:0:5760:wait" # Check for latency simulation flag -if [[ "$1" == "--with-latency" ]]; then +if [[ "${1:-}" == "--with-latency" ]]; then echo "" echo "Starting SITL with simulated latency (100ms round-trip)..." echo "This simulates Herelink-like network conditions." diff --git a/tools/tests/test_common.py b/tools/tests/test_common.py index 386d3418f57c..ec4d5342a56c 100644 --- a/tools/tests/test_common.py +++ b/tools/tests/test_common.py @@ -1,16 +1,10 @@ #!/usr/bin/env python3 """Tests for common utilities.""" -import sys from pathlib import Path import pytest -TOOLS_DIR = Path(__file__).parent.parent - -# Add tools to path for imports -sys.path.insert(0, str(TOOLS_DIR)) - from common.patterns import ( FACT_MEMBER_PATTERN, FACTGROUP_CLASS_PATTERN, diff --git a/tools/tests/test_coverage.py b/tools/tests/test_coverage.py new file mode 100644 index 000000000000..6e2be0f2627b --- /dev/null +++ b/tools/tests/test_coverage.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +"""Tests for tools/coverage.py.""" + +from __future__ import annotations + +from coverage import build_step_summary + + +def test_build_step_summary_includes_metrics() -> None: + markdown = build_step_summary("lines: 42.0% (42 out of 100)\nbranches: 10.0% (1 out of 10)\n", "report-only") + assert "| Lines | 42.0% (42 out of 100) |" in markdown + assert "| Branches | 10.0% (1 out of 10) |" in markdown diff --git a/tools/tests/test_download_artifacts.py b/tools/tests/test_download_artifacts.py index 8201e6f7717d..255225ee916b 100644 --- a/tools/tests/test_download_artifacts.py +++ b/tools/tests/test_download_artifacts.py @@ -5,13 +5,9 @@ import json import subprocess -import sys from pathlib import Path from unittest.mock import patch -TOOLS_DIR = Path(__file__).parent.parent -sys.path.insert(0, str(TOOLS_DIR)) - from setup import download_artifacts as mod @@ -25,6 +21,7 @@ def test_parse_args_defaults() -> None: assert args.head_sha == "abc123" assert args.output_dir == Path("artifacts") assert args.workflows == "Linux,Windows,MacOS,Android" + assert args.event == "" assert args.artifact_prefixes == "" assert args.artifact_metadata_out == "" @@ -37,7 +34,7 @@ def test_get_workflow_runs_filters_by_name_status_and_conclusion() -> None: {"id": 4, "name": "Other", "status": "completed", "conclusion": "success"}, ] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux", "Windows"]) assert len(runs) == 1 @@ -70,7 +67,7 @@ def test_get_workflow_runs_keeps_latest_successful_run_per_workflow() -> None: }, ] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux", "Windows"]) by_name = {run["name"]: run["id"] for run in runs} @@ -78,6 +75,18 @@ def test_get_workflow_runs_keeps_latest_successful_run_per_workflow() -> None: assert by_name["Windows"] == 20 +def test_get_workflow_runs_filters_by_event() -> None: + all_runs = [ + {"id": 10, "name": "Linux", "status": "completed", "conclusion": "success", "event": "push"}, + {"id": 11, "name": "Linux", "status": "completed", "conclusion": "success", "event": "pull_request"}, + ] + + with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): + runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux"], event="pull_request") + + assert [run["id"] for run in runs] == [11] + + def test_download_run_artifacts_failure_returns_false() -> None: with patch.object(mod, "gh", return_value=_cp(stderr="bad", returncode=1)): ok = mod.download_run_artifacts(123, "owner/repo", Path("artifacts")) @@ -107,7 +116,7 @@ def test_select_artifact_names_for_run_filters_by_prefix() -> None: {"name": "emulator-diagnostics-123"}, {"name": "QGroundControl-x86_64.AppImage"}, ] - with patch.object(mod._gh_actions, "list_run_artifacts", return_value=artifacts): + with patch.object(mod, "list_run_artifacts", return_value=artifacts): names = mod.select_artifact_names_for_run( "owner/repo", 42, @@ -129,7 +138,7 @@ def test_list_downloaded_artifacts_filters_extensions(tmp_path: Path) -> None: def test_main_returns_zero_when_no_runs_found() -> None: - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=[]), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=[]), patch.object( mod, "select_latest_successful_runs", return_value=[], ): rc = mod.main(["--repo", "owner/repo", "--head-sha", "abc123"]) @@ -138,7 +147,7 @@ def test_main_returns_zero_when_no_runs_found() -> None: def test_main_returns_one_when_downloads_fail_and_no_files(tmp_path: Path) -> None: runs = [{"id": 42, "name": "Linux", "status": "completed", "conclusion": "success"}] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( mod, "download_run_artifacts", return_value=False, @@ -156,10 +165,10 @@ def test_main_returns_one_when_downloads_fail_and_no_files(tmp_path: Path) -> No def test_main_returns_two_when_no_artifacts_match_prefixes(tmp_path: Path) -> None: runs = [{"id": 42, "name": "Linux", "status": "completed", "conclusion": "success"}] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( - mod._gh_actions, "list_run_artifacts", return_value=[{"name": "unrelated-artifact", "size_in_bytes": 1}], + mod, "list_run_artifacts", return_value=[{"name": "unrelated-artifact", "size_in_bytes": 1}], ), patch.object( mod, "download_run_artifacts", return_value=True, ), patch.object( @@ -233,8 +242,8 @@ def _artifacts_for_run(repo: str, run_id: int) -> list[dict[str, object]]: downloaded = tmp_path / "coverage.xml" downloaded.write_text("", encoding="utf-8") - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( - mod._gh_actions, "list_run_artifacts", side_effect=_artifacts_for_run, + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( + mod, "list_run_artifacts", side_effect=_artifacts_for_run, ), patch.object( mod, "download_run_artifacts", return_value=True, ) as download_mock, patch.object( @@ -264,10 +273,10 @@ def test_main_writes_artifact_metadata_file(tmp_path: Path) -> None: downloaded = tmp_path / "dummy.txt" downloaded.write_text("x", encoding="utf-8") - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( - mod._gh_actions, "list_run_artifacts", return_value=artifacts, + mod, "list_run_artifacts", return_value=artifacts, ), patch.object( mod, "download_run_artifacts", return_value=True, ), patch.object( diff --git a/tools/tests/test_factgroup_generator.py b/tools/tests/test_factgroup_generator.py index 50142b18df27..aac66c37fb9e 100644 --- a/tools/tests/test_factgroup_generator.py +++ b/tools/tests/test_factgroup_generator.py @@ -8,12 +8,8 @@ import pytest -TOOLS_DIR = Path(__file__).parent.parent FIXTURES_DIR = Path(__file__).parent / "fixtures" -# Add tools to path for imports -sys.path.insert(0, str(TOOLS_DIR)) - from generators.factgroup.generator import ( FactSpec, MavlinkMessageSpec, @@ -281,7 +277,8 @@ class TestCLI: def run_cli(self, *args): """Run the CLI and return result.""" cmd = [sys.executable, "-m", "tools.generators.factgroup.cli"] + list(args) - return subprocess.run(cmd, capture_output=True, text=True, cwd=TOOLS_DIR.parent) + repo_root = Path(__file__).resolve().parent.parent.parent + return subprocess.run(cmd, capture_output=True, text=True, cwd=repo_root) def test_cli_help(self): """--help should show usage.""" diff --git a/tools/tests/test_generate_docs.py b/tools/tests/test_generate_docs.py new file mode 100644 index 000000000000..53e7daf390ab --- /dev/null +++ b/tools/tests/test_generate_docs.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Tests for tools/generate_docs.py.""" + +from __future__ import annotations + +from generate_docs import build_doxyfile_text + + +def test_build_doxyfile_text_overrides_output_and_pdf(tmp_path) -> None: + base = "OUTPUT_DIRECTORY = docs/api\nGENERATE_LATEX = NO\n" + rendered = build_doxyfile_text(base, tmp_path / "api", generate_pdf=True) + + assert 'OUTPUT_DIRECTORY = "' in rendered + assert (tmp_path / "api").as_posix() in rendered + assert "GENERATE_LATEX = YES" in rendered + diff --git a/tools/tests/test_gh_actions.py b/tools/tests/test_gh_actions.py index bd75e5fa1887..b66b974f5f7a 100644 --- a/tools/tests/test_gh_actions.py +++ b/tools/tests/test_gh_actions.py @@ -6,13 +6,8 @@ import json import os import subprocess -import sys -from pathlib import Path from unittest.mock import patch -TOOLS_DIR = Path(__file__).parent.parent -sys.path.insert(0, str(TOOLS_DIR)) - from common import gh_actions as mod diff --git a/tools/tests/test_install_dependencies.py b/tools/tests/test_install_dependencies.py index e8f1e1d7ff49..a213132453b4 100644 --- a/tools/tests/test_install_dependencies.py +++ b/tools/tests/test_install_dependencies.py @@ -3,26 +3,24 @@ from __future__ import annotations -import sys from pathlib import Path from unittest.mock import MagicMock, call, patch import pytest -TOOLS_DIR = Path(__file__).parent.parent -sys.path.insert(0, str(TOOLS_DIR)) - from setup.install_dependencies import ( DEBIAN_PACKAGES, MACOS_PACKAGES, PIPX_PACKAGES, detect_platform, + get_available_debian_packages, get_apt_install_command, get_apt_update_command, get_debian_packages, get_macos_packages, parse_args, run_apt_install_with_retry, + validate_extra_packages, ) @@ -75,6 +73,11 @@ def test_get_debian_packages_no_duplicates() -> None: assert len(pkgs) == len(set(pkgs)) +def test_get_available_debian_packages_filters_unavailable() -> None: + with patch("setup.install_dependencies.check_apt_package_available", side_effect=lambda pkg: pkg == "cmake"): + assert get_available_debian_packages("core") == ["cmake"] + + def test_get_macos_packages() -> None: pkgs = get_macos_packages() assert "cmake" in pkgs @@ -145,6 +148,11 @@ def test_parse_args_category() -> None: assert args.category == "qt" +def test_parse_args_validate_extra_packages() -> None: + args = parse_args(["--validate-extra-packages", "foo", "bar"]) + assert args.validate_extra_packages == ["foo", "bar"] + + def test_parse_args_gstreamer_version() -> None: args = parse_args(["--platform", "windows", "--gstreamer-version", "1.24.0"]) assert args.gstreamer_version == "1.24.0" @@ -160,6 +168,15 @@ def test_parse_args_vulkan() -> None: assert args.vulkan is True +def test_validate_extra_packages_accepts_valid_names() -> None: + assert validate_extra_packages(["foo", "libbar-dev", "baz+1"]) == ["foo", "libbar-dev", "baz+1"] + + +def test_validate_extra_packages_rejects_invalid_names() -> None: + with pytest.raises(ValueError, match="Invalid package name"): + validate_extra_packages(["good", "bad;rm"]) + + # --------------------------------------------------------------------------- # download_file (network call mocked) # --------------------------------------------------------------------------- diff --git a/tools/tests/test_install_python.py b/tools/tests/test_install_python.py index 606c55b11e9d..98ab77db5b31 100644 --- a/tools/tests/test_install_python.py +++ b/tools/tests/test_install_python.py @@ -3,15 +3,12 @@ from __future__ import annotations -import sys from pathlib import Path +from unittest.mock import patch import pytest -TOOLS_DIR = Path(__file__).parent.parent -sys.path.insert(0, str(TOOLS_DIR)) - -from setup.install_python import get_packages_for_groups +from setup.install_python import get_packages_for_groups, sync_groups_with_uv def test_test_group_contains_pytest() -> None: @@ -21,6 +18,11 @@ def test_test_group_contains_pytest() -> None: assert "pyyaml" in packages +def test_scripts_group_contains_defusedxml() -> None: + packages = get_packages_for_groups("scripts") + assert "defusedxml>=0.7.1" in packages + + def test_multiple_groups_are_merged() -> None: packages = get_packages_for_groups("precommit,test") assert "pre-commit" in packages @@ -39,3 +41,18 @@ def test_all_group_includes_test_and_ci_tools() -> None: def test_unknown_group_raises() -> None: with pytest.raises(ValueError, match="Unknown group"): get_packages_for_groups("nope") + + +def test_sync_groups_with_uv_uses_locked_active_env(tmp_path: Path) -> None: + venv = tmp_path / ".venv" + (tmp_path / "tools").mkdir() + lockfile = tmp_path / "tools" / "uv.lock" + lockfile.write_text("", encoding="utf-8") + with patch("setup.install_python.find_repo_root", return_value=tmp_path), \ + patch("setup.install_python.subprocess.run") as mock_run: + sync_groups_with_uv(venv, "scripts,test") + + args = mock_run.call_args.args[0] + assert args[:5] == ["uv", "sync", "--project", str(tmp_path / "tools"), "--active"] + assert "--frozen" in args + assert args.count("--extra") == 2 diff --git a/tools/tests/test_pre_commit.py b/tools/tests/test_pre_commit.py new file mode 100644 index 000000000000..4f3ecbdbe001 --- /dev/null +++ b/tools/tests/test_pre_commit.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Tests for tools/pre_commit.py.""" + +from __future__ import annotations + +from unittest.mock import patch + +from pre_commit import build_precommit_args, extract_hook_lines, parse_args, summarize_output + + +def test_summarize_output_counts_states() -> None: + passed, failed, skipped = summarize_output( + "hook-a........................Passed\nhook-b........................Failed\nhook-c........................Skipped\n" + ) + assert (passed, failed, skipped) == (1, 1, 1) + + +def test_extract_hook_lines_strips_ansi() -> None: + lines = extract_hook_lines("\x1b[31mhook-a........................Failed\x1b[0m\n") + assert lines == ["hook-a........................Failed"] + + +def test_build_precommit_args_all_files() -> None: + args = parse_args([]) + with patch("pre_commit.git_has_master_ref", return_value=True): + assert build_precommit_args(args)[-1] == "--all-files" + + +def test_build_precommit_args_changed_mode() -> None: + args = parse_args(["--changed"]) + with patch("pre_commit.git_has_master_ref", return_value=True): + built = build_precommit_args(args) + assert built[-4:] == ["--from-ref", "master", "--to-ref", "HEAD"] diff --git a/tools/tests/test_resolve_gstreamer_config.py b/tools/tests/test_resolve_gstreamer_config.py new file mode 100644 index 000000000000..28273bf3d5fc --- /dev/null +++ b/tools/tests/test_resolve_gstreamer_config.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Tests for tools/setup/resolve_gstreamer_config.py.""" + +from __future__ import annotations + +from unittest.mock import patch + +from setup.resolve_gstreamer_config import resolve_version + + +def test_resolve_version_prefers_explicit_override() -> None: + assert resolve_version("linux", "1.2.3") == "1.2.3" + + +def test_resolve_version_uses_platform_specific_key() -> None: + with patch("setup.resolve_gstreamer_config.get_build_config_value", return_value="9.9.9") as mock_get: + assert resolve_version("windows", "") == "9.9.9" + mock_get.assert_called_once() + diff --git a/tools/translations/qgc-lupdate-json.py b/tools/translations/qgc-lupdate-json.py index e9665f2c4bed..e900b2972a60 100755 --- a/tools/translations/qgc-lupdate-json.py +++ b/tools/translations/qgc-lupdate-json.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import json import os import sys diff --git a/tools/uv.lock b/tools/uv.lock new file mode 100644 index 000000000000..eddafc51fd85 --- /dev/null +++ b/tools/uv.lock @@ -0,0 +1,1256 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] + +[[package]] +name = "aqtinstall" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bs4" }, + { name = "defusedxml" }, + { name = "humanize" }, + { name = "patch-ng" }, + { name = "py7zr" }, + { name = "requests" }, + { name = "semantic-version" }, + { name = "texttable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/19/24a588de6c25d43169d172dab47e63a63cd0d8f90e98cf86487acbf00ac7/aqtinstall-3.3.0.tar.gz", hash = "sha256:9c7d85fbe7258be2d7d23fda33f8aff2e8b7536817255eaeaaf4226da8546a31", size = 374818, upload-time = "2025-06-02T11:10:53.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/70/849fa1005fa197fc28a744fd69598ab9da883b737a1ab9461d88721ddd0b/aqtinstall-3.3.0-py3-none-any.whl", hash = "sha256:e88dbd87226f276fdd5d05347a44578d390d93a7f176e9476fbda0a7c9635f69", size = 71789, upload-time = "2025-06-02T11:10:51.994Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backports-zstd" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/36a5182ce1d8ef9ef32bff69037bd28b389bbdb66338f8069e61da7028cb/backports_zstd-1.3.0.tar.gz", hash = "sha256:e8b2d68e2812f5c9970cabc5e21da8b409b5ed04e79b4585dbffa33e9b45ebe2", size = 997138, upload-time = "2025-12-29T17:28:06.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/d4/356da49d3053f4bc50e71a8535631b57bc9ca4e8c6d2442e073e0ab41c44/backports_zstd-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f4a292e357f3046d18766ce06d990ccbab97411708d3acb934e63529c2ea7786", size = 435972, upload-time = "2025-12-29T17:26:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/30/8f/dbe389e60c7e47af488520f31a4aa14028d66da5bf3c60d3044b571eb906/backports_zstd-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb4c386f38323698991b38edcc9c091d46d4713f5df02a3b5c80a28b40e289ea", size = 362124, upload-time = "2025-12-29T17:26:19.995Z" }, + { url = "https://files.pythonhosted.org/packages/55/4b/173beafc99e99e7276ce008ef060b704471e75124c826bc5e2092815da37/backports_zstd-1.3.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f52523d2bdada29e653261abdc9cfcecd9e5500d305708b7e37caddb24909d4e", size = 506378, upload-time = "2025-12-29T17:26:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/df/c8/3f12a411d9a99d262cdb37b521025eecc2aa7e4a93277be3f4f4889adb74/backports_zstd-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3321d00beaacbd647252a7f581c1e1cdbdbda2407f2addce4bfb10e8e404b7c7", size = 476201, upload-time = "2025-12-29T17:26:23.047Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/73c090e4a2d5671422512e1b6d276ca6ea0cc0c45ec4634789106adc0d66/backports_zstd-1.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88f94d238ef36c639c0ae17cf41054ce103da9c4d399c6a778ce82690d9f4919", size = 581659, upload-time = "2025-12-29T17:26:24.189Z" }, + { url = "https://files.pythonhosted.org/packages/08/4f/11bfcef534aa2bf3f476f52130217b45337f334d8a287edb2e06744a6515/backports_zstd-1.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97d8c78fe20c7442c810adccfd5e3ea6a4e6f4f1fa4c73da2bc083260ebead17", size = 640388, upload-time = "2025-12-29T17:26:25.47Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8faea426d4f49b63238bdfd9f211a9f01c862efe0d756d3abeb84265a4e2/backports_zstd-1.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eefda80c3dbfbd924f1c317e7b0543d39304ee645583cb58bae29e19f42948ed", size = 494173, upload-time = "2025-12-29T17:26:26.736Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9d/901f19ac90f3cd999bdcfb6edb4d7b4dc383dfba537f06f533fc9ac4777b/backports_zstd-1.3.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ab5d3b5a54a674f4f6367bb9e0914063f22cd102323876135e9cc7a8f14f17e", size = 568628, upload-time = "2025-12-29T17:26:28.12Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/4d29788590c2465a570c2fae49dbff05741d1f0c8e4a0fb2c1c310f31804/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7558fb0e8c8197c59a5f80c56bf8f56c3690c45fd62f14e9e2081661556e3e64", size = 482233, upload-time = "2025-12-29T17:26:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4b/24c7c9e8ef384b19d515a7b1644a500ceb3da3baeff6d579687da1a0f62b/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27744870e38f017159b9c0241ea51562f94c7fefcfa4c5190fb3ec4a65a7fc63", size = 509806, upload-time = "2025-12-29T17:26:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7e/7ba1aeecf0b5859f1855c0e661b4559566b64000f0627698ebd9e83f2138/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b099750755bb74c280827c7d68de621da0f245189082ab48ff91bda0ec2db9df", size = 586037, upload-time = "2025-12-29T17:26:32.201Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/18f0402b36b9cfb0aea010b5df900cfd42c214f37493561dba3abac90c4e/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5434e86f2836d453ae3e19a2711449683b7e21e107686838d12a255ad256ca99", size = 566220, upload-time = "2025-12-29T17:26:33.5Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d9/44c098ab31b948bbfd909ec4ae08e1e44c5025a2d846f62991a62ab3ebea/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:407e451f64e2f357c9218f5be4e372bb6102d7ae88582d415262a9d0a4f9b625", size = 630847, upload-time = "2025-12-29T17:26:35.273Z" }, + { url = "https://files.pythonhosted.org/packages/30/33/e74cb2cfb162d2e9e00dad8bcdf53118ca7786cfd467925d6864732f79cc/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58a071f3c198c781b2df801070290b7174e3ff61875454e9df93ab7ea9ea832b", size = 498665, upload-time = "2025-12-29T17:26:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a9/67a24007c333ed22736d5cd79f1aa1d7209f09be772ff82a8fd724c1978e/backports_zstd-1.3.0-cp312-cp312-win32.whl", hash = "sha256:21a9a542ccc7958ddb51ae6e46d8ed25d585b54d0d52aaa1c8da431ea158046a", size = 288809, upload-time = "2025-12-29T17:26:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/42/24/34b816118ea913debb2ea23e71ffd0fb2e2ac738064c4ac32e3fb62c18bb/backports_zstd-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:89ea8281821123b071a06b30b80da8e4d8a2b40a4f57315a19850337a21297ac", size = 313815, upload-time = "2025-12-29T17:26:39.665Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2f/babd02c9fc4ca35376ada7c291193a208165c7be2455f0f98bc1e1243f31/backports_zstd-1.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:f6843ecb181480e423b02f60fe29e393cbc31a95fb532acdf0d3a2c87bd50ce3", size = 288927, upload-time = "2025-12-29T17:26:40.923Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7d/53e8da5950cdfc5e8fe23efd5165ce2f4fed5222f9a3292e0cdb03dd8c0d/backports_zstd-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e86e03e3661900955f01afed6c59cae9baa63574e3b66896d99b7de97eaffce9", size = 435463, upload-time = "2025-12-29T17:26:42.152Z" }, + { url = "https://files.pythonhosted.org/packages/da/78/f98e53870f7404071a41e3d04f2ff514302eeeb3279d931d02b220f437aa/backports_zstd-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:41974dcacc9824c1effe1c8d2f9d762bcf47d265ca4581a3c63321c7b06c61f0", size = 361740, upload-time = "2025-12-29T17:26:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ed/2c64706205a944c9c346d95c17f632d4e3468db3ce60efb6f5caa7c0dcae/backports_zstd-1.3.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:3090a97738d6ce9545d3ca5446df43370928092a962cbc0153e5445a947e98ed", size = 505651, upload-time = "2025-12-29T17:26:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7b/22998f691dc6e0c7e6fa81d611eb4b1f6a72fb27327f322366d4a7ca8fb3/backports_zstd-1.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc874638abf03ea1ff3b0525b4a26a8d0adf7cb46a448c3449f08e4abc276b3", size = 475859, upload-time = "2025-12-29T17:26:45.722Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/0cde898339a339530e5f932634872d2d64549969535447a48d3b98959e11/backports_zstd-1.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:db609e57b8ed88b3472930c87e93c08a4bbd5ffeb94608cd9c7c6f0ac0e166c6", size = 581339, upload-time = "2025-12-29T17:26:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1d/e0973e0eebe678c12c146473af2c54cda8a3e63b179785ca1a20727ad69c/backports_zstd-1.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f13033a3dd95f323c067199f2e61b4589a7880188ef4ef356c7ffbdb78a9f11", size = 642182, upload-time = "2025-12-29T17:26:48.545Z" }, + { url = "https://files.pythonhosted.org/packages/82/a2/ac67e79e137eb98aead66c7162bafe3cffcb82ef9cdeb6367ec18d88fbce/backports_zstd-1.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c4c7bcda5619a754726e7f5b391827f5efbe4bed8e62e9ec7490d42bff18aa6", size = 490807, upload-time = "2025-12-29T17:26:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/3514b1d065801ae7dce05246e9389003ed8fb1d7c3d71f85aa07a80f41e6/backports_zstd-1.3.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884a94c40f27affe986f394f219a4fd3cbbd08e1cff2e028d29d467574cd266e", size = 566103, upload-time = "2025-12-29T17:26:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/1b/03/10ddb54cbf032e5fe390c0776d3392611b1fc772d6c3cb5a9bcdff4f915f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497f5765126f11a5b3fd8fedfdae0166d1dd867e7179b8148370a3313d047197", size = 481614, upload-time = "2025-12-29T17:26:52.255Z" }, + { url = "https://files.pythonhosted.org/packages/5c/13/21efa7f94c41447f43aee1563b05fc540a235e61bce4597754f6c11c2e97/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a6ff6769948bb29bba07e1c2e8582d5a9765192a366108e42d6581a458475881", size = 509207, upload-time = "2025-12-29T17:26:53.496Z" }, + { url = "https://files.pythonhosted.org/packages/de/e7/12da9256d9e49e71030f0ff75e9f7c258e76091a4eaf5b5f414409be6a57/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1623e5bff1acd9c8ef90d24fc548110f20df2d14432bfe5de59e76fc036824ef", size = 585765, upload-time = "2025-12-29T17:26:54.99Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/59ca9cb4e7be1e59331bb792e8ef1331828efe596b1a2f8cbbc4e3f70d75/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:622c28306dcc429c8f2057fc4421d5722b1f22968d299025b35d71b50cfd4e03", size = 563852, upload-time = "2025-12-29T17:26:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ee/5a3eaed9a73bdf2c35dc0c7adc0616a99588e0de28f5ab52f3e0caaaa96f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09a2785e410ed2e812cb39b684ef5eb55083a5897bfd0e6f5de3bbd2c6345f70", size = 632549, upload-time = "2025-12-29T17:26:57.598Z" }, + { url = "https://files.pythonhosted.org/packages/75/b9/c823633afc48a1ac56d6ad34289c8f51b0234685142531bfa8197ca91777/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ade1f4127fdbe36a02f8067d75aa79c1ea1c8a306bf63c7b818bb7b530e1beaa", size = 495104, upload-time = "2025-12-29T17:26:58.826Z" }, + { url = "https://files.pythonhosted.org/packages/a3/8f/6f7030f18fa7307f87b0f57108a50a3a540b6350e2486d1739c0567629a3/backports_zstd-1.3.0-cp313-cp313-win32.whl", hash = "sha256:668e6fb1805b825cb7504c71436f7b28d4d792bb2663ee901ec9a2bb15804437", size = 288447, upload-time = "2025-12-29T17:27:00.036Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/b1df1bbbe4e6d3ffd364d0bcffdeb6c4361115c1eccd91238dbdd0c07fec/backports_zstd-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:385bdadf0ea8fe6ba780a95e4c7d7f018db7bafdd630932f0f9f0fad05d608ff", size = 313664, upload-time = "2025-12-29T17:27:01.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/0f/60918fe4d3f2881de8f4088d73be4837df9e4c6567594109d355a2d548b6/backports_zstd-1.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:4321a8a367537224b3559fe7aeb8012b98aea2a60a737e59e51d86e2e856fe0a", size = 288678, upload-time = "2025-12-29T17:27:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/35f423c0bcd85020d5e7be6ab8d7517843e3e4441071beb5c3bd8c5216cb/backports_zstd-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:10057d66fa4f0a7d3f6419ffb84b4fe61088da572e3ac4446134a1c8089e4166", size = 436155, upload-time = "2025-12-29T17:27:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/e504daea24e8916f14ecbc223c354b558d8410cfc846606668ab91d96b38/backports_zstd-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4abf29d706ba05f658ca0247eb55675bcc00e10f12bca15736e45b05f1f2d2dc", size = 362436, upload-time = "2025-12-29T17:27:05.076Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f7/06e178dbab7edb88c2872aebd68b54137e07a169eba1aeedf614014f7036/backports_zstd-1.3.0-cp313-cp313t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:127b0d73c745b0684da3d95c31c0939570810dad8967dfe8231eea8f0e047b2f", size = 507600, upload-time = "2025-12-29T17:27:06.254Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f1/2ce499b81c4389d6fa1eeea7e76f6e0bad48effdbb239da7cbcdaaf24b76/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0205ef809fb38bb5ca7f59fa03993596f918768b9378fb7fbd8a68889a6ce028", size = 475496, upload-time = "2025-12-29T17:27:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/1e/c82a586f2866aabf3a601a521af3c58756d83d98b724fda200016ac5e7e2/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c389b667b0b07915781aa28beabf2481f11a6062a1a081873c4c443b98601a7", size = 580919, upload-time = "2025-12-29T17:27:09.1Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/eb5d9b7c4cb69d1b8ccd011abe244ba6815693b70bed07ed4b77ddda4535/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8e7ac5ef693d49d6fb35cd7bbb98c4762cfea94a8bd2bf2ab112027004f70b11", size = 639913, upload-time = "2025-12-29T17:27:10.433Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/7296b99df79d9f31174a99c81c1964a32de8996ce2b3068f5bc66b413615/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d5543945aae2a76a850b23f283249424f535de6a622d6002957b7d971e6a36d", size = 494800, upload-time = "2025-12-29T17:27:11.59Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fc/b8ae6e104ba72d20cd5f9dfd9baee36675e89c81d432434927967114f30f/backports_zstd-1.3.0-cp313-cp313t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38be15ebce82737deda2c9410c1f942f1df9da74121049243a009810432db75", size = 570396, upload-time = "2025-12-29T17:27:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/56/60a7a9de7a5bc951ea1106358b413c95183c93480394f3abc541313c8679/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3e3f58c76f4730607a4e0130d629173aa114ae72a5c8d3d5ad94e1bf51f18d8", size = 481980, upload-time = "2025-12-29T17:27:14.317Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/93fc1e8e81b8ecba58b0e53a14f7b44375cf837db6354410998f0c4cb6ff/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b808bf889722d889b792f7894e19c1f904bb0e9092d8c0eb0787b939b08bad9a", size = 511358, upload-time = "2025-12-29T17:27:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0f/b165c2a6080d22306975cd86ce97270208493f31a298867e343110570370/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f7be27d56f2f715bcd252d0c65c232146d8e1e039c7e2835b8a3ad3dc88bc508", size = 585492, upload-time = "2025-12-29T17:27:16.986Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/85b4bde76e982b24a7eb57a2fb9868807887bef4d2114a3654a6530a67ef/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:cbe341c7fcc723893663a37175ba859328b907a4e6d2d40a4c26629cc55efb67", size = 568309, upload-time = "2025-12-29T17:27:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/83/64/9490667827a320766fb883f358a7c19171fdc04f19ade156a8c341c36967/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b4116a9e12dfcd834dd9132cf6a94657bf0d328cba5b295f26de26ea0ae1adc8", size = 630518, upload-time = "2025-12-29T17:27:19.525Z" }, + { url = "https://files.pythonhosted.org/packages/ea/43/258587233b728bbff457bdb0c52b3e08504c485a8642b3daeb0bdd5a76bc/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1049e804cc8754290b24dab383d4d6ed0b7f794ad8338813ddcb3907d15a89d0", size = 499429, upload-time = "2025-12-29T17:27:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/32/04/cfab76878f360f124dbb533779e1e4603c801a0f5ada72ae5c742b7c4d7d/backports_zstd-1.3.0-cp313-cp313t-win32.whl", hash = "sha256:7d3f0f2499d2049ec53d2674c605a4b3052c217cc7ee49c05258046411685adc", size = 289389, upload-time = "2025-12-29T17:27:22.287Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ff/dbcfb6c9c922ab6d98f3d321e7d0c7b34ecfa26f3ca71d930fe1ef639737/backports_zstd-1.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eb2f8fab0b1ea05148394cb34a9e543a43477178765f2d6e7c84ed332e34935e", size = 314776, upload-time = "2025-12-29T17:27:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/01/4b/82e4baae3117806639fe1c693b1f2f7e6133a7cefd1fa2e38018c8edcd68/backports_zstd-1.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c66ad9eb5bfbe28c2387b7fc58ddcdecfb336d6e4e60bcba1694a906c1f21a6c", size = 289315, upload-time = "2025-12-29T17:27:24.601Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, +] + +[[package]] +name = "brotlicffi" +version = "1.2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/f9/dfa56316837fa798eac19358351e974de8e1e2ca9475af4cb90293cd6576/brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd", size = 433046, upload-time = "2026-03-05T19:53:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f5/f8f492158c76b0d940388801f04f747028971ad5774287bded5f1e53f08d/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5", size = 1541126, upload-time = "2026-03-05T19:53:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e1/ff87af10ac419600c63e9287a0649c673673ae6b4f2bcf48e96cb2f89f60/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac", size = 1541983, upload-time = "2026-03-05T19:53:50.317Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/80ecd9bd45776109fab14040e478bf63e456967c9ddee2353d8330ed8de1/brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec", size = 349047, upload-time = "2026-03-05T19:53:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/98/13e5b250236a281b6cd9e92a01ee1ae231029fa78faee932ef3766e1cb24/brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000", size = 385652, upload-time = "2026-03-05T19:53:53.892Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" }, +] + +[[package]] +name = "bs4" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698, upload-time = "2024-01-17T18:15:47.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189, upload-time = "2024-01-17T18:15:48.613Z" }, +] + +[[package]] +name = "cattrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "fastcrc" +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/79/0afaff8ff928ce9990ca883998c4ea7a7f07f2dfea3ebd6d65ba2aadfd4e/fastcrc-0.3.5.tar.gz", hash = "sha256:3705cbad6b3f283a04256f97ae899404794395090ff5966eac79fe303c13e93e", size = 11979, upload-time = "2025-12-31T18:23:09.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/6e/d57d65fb5a36fcbf6d35f759172ebf18646c2abdc3ce5300d4b1c010337a/fastcrc-0.3.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6d0a131010c608f46ad2ab1aea2b4ec2a01be90f18af8ff94b58ded7043d123e", size = 255680, upload-time = "2025-12-31T18:21:47.841Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/56a568a74f35bbd0b6b4b983b39de06ba7a21bc0502545a63d9eca8004a3/fastcrc-0.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3302c42d6356da9b0cad35e9cebff262c4542a5cdace98857a16bf7203166ed", size = 251091, upload-time = "2025-12-31T18:21:42.197Z" }, + { url = "https://files.pythonhosted.org/packages/27/bf/a7412fef1676e98ba07cb64b8a7b5b721a5b63f8fc6cccad215e52b4ac67/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8883e1ad3e530f9c97f41fbee35ae555876186475918fd42da5f78e6da674322", size = 282008, upload-time = "2025-12-31T18:19:48.897Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e1/a70192cccd292a977998ff44150cf12680dc82b9f706df89f757903d275f/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15ada82abfc4d6e775b61d4591b42ce2c95994ab9139bc31ba2085ba05b32135", size = 290131, upload-time = "2025-12-31T18:20:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/e2/16/5d1ac72c26494a7eb9ced6034bbdde1efbbbfbb1275c70e70748188e622b/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:402c4663ecb5eb00d2ddb58407796cfb731f72e9e348f152809d6292c5229ba7", size = 407328, upload-time = "2025-12-31T18:20:26.344Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/c54608230fede639d3d443cd6fd08cf53457fe347f13649112816d94fd66/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4270ff2603b6d5bdac43c92e317605cb921f605a05d383917222ada402ed0b8e", size = 307459, upload-time = "2025-12-31T18:21:03.396Z" }, + { url = "https://files.pythonhosted.org/packages/c3/37/379fae277f2b73d0703996c5b78e5ebdde1a346d8c4d5bb9a6fb2fa4df6b/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9088dc6f0ff21535fd29ff4639ce5e7b5cb4666fe30040fbfe29614c46ef6c7", size = 286617, upload-time = "2025-12-31T18:21:32.338Z" }, + { url = "https://files.pythonhosted.org/packages/d4/25/e389d565cac63d620e9e501ee3b297b01144165eaef9fdd616fbfa1fdbd0/fastcrc-0.3.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:4b5860aaf5e1114731b63e924be35179b570016ac3fcdd051265c5665c34efa9", size = 290784, upload-time = "2025-12-31T18:20:44.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/f8/2857b9e0076d4d5838f9393e5d47624fe28b9c6f154697c8e420249f3c4e/fastcrc-0.3.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a69b53da8ffbfe60099555a9f38ebb05519ba76233caf0f866ac5943efd1df3", size = 299395, upload-time = "2025-12-31T18:21:21.254Z" }, + { url = "https://files.pythonhosted.org/packages/fb/60/e69d182d150f41ca8db07c0ba5d77d371ee52ebce13537d1d487a45980aa/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a9d2b2afcfab28dbc9fa4aa516a4570524cb74d0aa734f0cf612bc9070c360d", size = 464295, upload-time = "2025-12-31T18:21:55.355Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/60f3379f11f55e2bda0f46e660b1ae772f3031e751649474c9ba7ad5e52d/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c96dbf431f7214d22329650121a5f0c41377722833f255f2d926d7df0d4b1143", size = 560169, upload-time = "2025-12-31T18:22:15.081Z" }, + { url = "https://files.pythonhosted.org/packages/76/5c/8bd19ba855624aea12a6c2da5cef2cf3d12119c55acd048349583c218a7d/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:186e3f5fdfa912b43cd9700dc6be5c5c126fe8e159eb65f0757a912e0db671d4", size = 518877, upload-time = "2025-12-31T18:22:33.91Z" }, + { url = "https://files.pythonhosted.org/packages/12/54/50b211283dc54f5af3517dee0b94797f04848c844a3765fd56a5aa899a0c/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4680abf2b254599d6d21bb58c1140e4a8142d951d94240c91cc30362c26c54c5", size = 489218, upload-time = "2025-12-31T18:22:52.785Z" }, + { url = "https://files.pythonhosted.org/packages/dd/af/b77460dbe826793fc65a39b4959efa677d12be6d6680cab6b24035b82da2/fastcrc-0.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:aa8614b0340be45b280c2c4f0330a546c71a2167c4414625096254969750b17b", size = 146970, upload-time = "2025-12-31T18:23:14.477Z" }, + { url = "https://files.pythonhosted.org/packages/45/13/f13eb8e644f18c621d1003d1e34209759ed28ace2eb698809558f45f831e/fastcrc-0.3.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:846aca923cc96965f41a9ebb5c2a4172d948d67285b2e6f2af495fda55d2d624", size = 255919, upload-time = "2025-12-31T18:21:49.158Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ab/72a8a7881f1ac1fd5ab8679fe29a57dbf0523d9c5ee9538da744d5e10d95/fastcrc-0.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fe5d56b33a68bc647242345484281e9df7818adb7c6f690393e318a413597872", size = 251366, upload-time = "2025-12-31T18:21:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3a/605dda593c0e089b9eaf8a6b614fd3da174ecd746c7ea1212033f1ff897a/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:953093ed390ad1601288d349a0f08a48562b6b547ee184c8396b88dff50a6a5f", size = 282465, upload-time = "2025-12-31T18:19:50.36Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cb/f0bb9501c96471ec415319b342995d823a2c9482bcebff705fead1e23224/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50679ec92e0c668b76bb1b9f5d48f7341fecc593419a60cce245f05e3631be10", size = 290304, upload-time = "2025-12-31T18:20:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6e/a59baef04c2e991e9a07494b585906aa23017d2262580c453cca29448270/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d377bb515176d08167daa5784f37f0372280042bde0974d6cdcf0874ce33fdc", size = 409004, upload-time = "2025-12-31T18:20:27.895Z" }, + { url = "https://files.pythonhosted.org/packages/86/97/9931fbc57226a83a76ee31fd197e5fb39979cb02cd975a598ab9c9a4e13d/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89e6f4df265d4443c45329857ddee4b445546c39d839e107dc471ba9398cde1d", size = 307402, upload-time = "2025-12-31T18:21:05.107Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2b/aea9bd3694fa323737cf6db7ac63a3fe21cc8987fe6d2b9f97531801688b/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65d6210030a84ed3aaec0736e18908b2e499784c51f6ffd0d8f7d96de90eea1", size = 286802, upload-time = "2025-12-31T18:21:34.406Z" }, + { url = "https://files.pythonhosted.org/packages/15/03/6667737b2a24bd48a15bea1924bed3d7cd322813bc61f5ee3ea7632417fa/fastcrc-0.3.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a1be96b4090518cd6718c24a0f012f7f385fabbd255ce6a7b0d8ec025c9fb377", size = 291114, upload-time = "2025-12-31T18:20:45.99Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/983934a2a56078a022e6f82f3fd6baf40d7e85871658590b57274179dc85/fastcrc-0.3.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63cf91059d8ab4fdb8875296cff961f7e30af81d1263ba11de4feabea980f932", size = 299618, upload-time = "2025-12-31T18:21:22.499Z" }, + { url = "https://files.pythonhosted.org/packages/e4/22/59cfae39201db940a1f97bb0fd8f5508c7065394a19a77cd6c5d6cbf2f6b/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5ad026ab9afe90effe55d2237d0357cc0edcfbb1b7cd7fa6c9c12b8649d30a9", size = 464682, upload-time = "2025-12-31T18:21:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4c/b129f316ddcbf4bf1c0745b209a45a5f2bf5bfd4ccd528893d3d25ce53f6/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2e4e0f053f9c683d11dea4282f9e0a9c5f0299883a4dd83e36eb0da83716f4f9", size = 560357, upload-time = "2025-12-31T18:22:16.694Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/c7342e8a966fb3bff7672b5727e08c54e509a470e4f96623cc5c6ff8679c/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:80fb2879c0e0bb1d20ea599e4033f48a50d3565550a484a4f3f020847d929569", size = 519044, upload-time = "2025-12-31T18:22:35.399Z" }, + { url = "https://files.pythonhosted.org/packages/02/b6/e65ba338709e49d205a612c69996b66af1bfd78e9c9278fffc0bea8613c3/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03ff342e97ff48f9f3c8aa12c48d87ed90b50b753d9cf65d2fecdb8a67cef20d", size = 489513, upload-time = "2025-12-31T18:22:54.496Z" }, + { url = "https://files.pythonhosted.org/packages/58/dd/a0338c32d8e404f32b26b6948d0b60cedc07e1fa76661c331931473ead71/fastcrc-0.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:6d59f686f253478626b9befa4dfff723d7ae5509d2aa507a1bf26cfd4ec05ae4", size = 147181, upload-time = "2025-12-31T18:23:16.014Z" }, + { url = "https://files.pythonhosted.org/packages/59/39/1a656464fca9c32722b36065cbf3ae989da8783e0d012102ade5e968b385/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2b9f060e14b291e35783372afb87227c7f1dc29f876b76f06c39552103b6545", size = 282367, upload-time = "2025-12-31T18:19:51.761Z" }, + { url = "https://files.pythonhosted.org/packages/fa/35/d9efe66a001f989e84e3d1d196f9cc8764dcead87f43e9f22b3f1ea6d1e1/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93e2761f46567334690c4becc8adcd33c3c8bd40c0f05b1a2b151265d57aff73", size = 289402, upload-time = "2025-12-31T18:20:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ca/c10d7fc598528052463d892c4e71c01c00985cfdb86500da3550fb0b6e75/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f07abfb775b6b7c4a55819847f9f9ddd6b710ebc5e822989860771f77a08bf9c", size = 408177, upload-time = "2025-12-31T18:20:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/c5/72/59bbfe8c6bdb17eb86a43a958268da3fefe3d0da67496e3112dfb84f276a/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5c57c4f70c59f6adafa8b3ae8ac1f60da42a87479d6e0eea04dbaa3c00aca6e", size = 307543, upload-time = "2025-12-31T18:21:06.85Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/904760e09f5768ecbf25da8fddf57fb4fb1599b92780294f7e6172e2a398/fastcrc-0.3.5-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:03f50409fbcb118e66739012a7debcfd55dd409d6d87c32986080efdf0a70144", size = 290841, upload-time = "2025-12-31T18:20:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/340e3d9525d442189883fce613134a5acf167d7f3e48d95036f1e0c9a2dc/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89e6247432e481c420daeb670751a496680484c0c6f69e2198522d6f0f6a5b3a", size = 464580, upload-time = "2025-12-31T18:21:58.673Z" }, + { url = "https://files.pythonhosted.org/packages/4c/97/e2a90908069c89ea41b1cf7ae956fb77219d293ebe62cca32d6c2a077c16/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:9d40dcef226068888898c30ef4005c22e697360685d7e40945365bee63e15d26", size = 559567, upload-time = "2025-12-31T18:22:18.133Z" }, + { url = "https://files.pythonhosted.org/packages/91/93/b8652fabe301faf050cd19d2f6ae655a61f743687fb8dfc8e614fbf9c493/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:edabc8ee13f7581e3fb42344999a35a6a253cb65ac019969dc38aa45dac32ee8", size = 518538, upload-time = "2025-12-31T18:22:36.82Z" }, + { url = "https://files.pythonhosted.org/packages/98/c9/585216c6a93b398b3c911653eacaf05c5dc731db39b55f99415025af90bc/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:430ae0104f9eafe893f232e81834144ba31041bcc63a3eb379d22d0978c6e926", size = 489898, upload-time = "2025-12-31T18:22:55.903Z" }, + { url = "https://files.pythonhosted.org/packages/57/aa/845d6257bac319b9c1fe16648f2e3d0efa1bbbaf8a5b7b4977851d8102ae/fastcrc-0.3.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b6507652630bc1076f57fe56f265634df21f811d6882a2723be854c58664318c", size = 255565, upload-time = "2025-12-31T18:21:50.502Z" }, + { url = "https://files.pythonhosted.org/packages/f7/66/f67a5e6bf89fa7de858fd055b5a69f00029c16cabf5dcf9bc750db120257/fastcrc-0.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a4c39b9c4a8a37f2fb0a255e3b63b640a5426c0daf3d790399ea85aaabad7f6", size = 251112, upload-time = "2025-12-31T18:21:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/74/a1/c7568f21ad284e68faed0093ccb68bb5d5b248bd08f6241dedfe69ff000b/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d0621722fc4c17bdd7e772fb3fb5384d7c985bb1c8f66256a1dba374e2d12a5", size = 282301, upload-time = "2025-12-31T18:19:53.016Z" }, + { url = "https://files.pythonhosted.org/packages/35/57/da0342f2702e6b50b4d4e5befb2fcd127e82762fe30925b9160eed2184a1/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:648d63f41da1216ef1143573fef35ad2eb34913496ccec58138c2619b10ea469", size = 289811, upload-time = "2025-12-31T18:20:11.584Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d6/94e24eb87bb02546b2b809a8c05034e1e90df3179a44260451e640533a9c/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2be6196f5c4a40b7fca04ef0cc176aa30ac2e19206f362b953fe0db00ea76080", size = 406010, upload-time = "2025-12-31T18:20:30.67Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/c4d71a07bcba4761db0c8ab70b4cb2d1fbd803f72d97e8cde188bd1cb658/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c70e985afa6ec16eebaeb0f3d6bfacb46826636f59853f1169e2eb2a336a2c5", size = 307190, upload-time = "2025-12-31T18:21:08.204Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/bba64d12b0c22d839ddb8cfafae49bb332ae925e839fff2b7752bb20d8dc/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562820b556d9d2070948a89cb76c34d6ec777bbcd3f70bdb89638a16b6072964", size = 286376, upload-time = "2025-12-31T18:21:35.986Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/4fab9f16bb9e6eb6d0c74f913691c25c6f332761c255edd5f3e22a57bd65/fastcrc-0.3.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:99a333fa78aa32f2a024528828cfad71aa39533f210d121ec3962e70542d857b", size = 290450, upload-time = "2025-12-31T18:20:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/41/2c/d4e4072c39f40c8a8550499722ab2539d1de1f30feb97f637d48d33325c7/fastcrc-0.3.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d752dc2285dc446fdf8e3d6e349a3428f46f7b8f059842bdabbb989332d1f3e", size = 299070, upload-time = "2025-12-31T18:21:23.761Z" }, + { url = "https://files.pythonhosted.org/packages/0d/37/12fe830bdfe3b39367645d5b2f8fb2359dc46e67346e91fdd7b9253ad501/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c996c9273c4d3b4c2902852a51b7142bd14a6c750f84bec47778225f7f8952c3", size = 464591, upload-time = "2025-12-31T18:22:00.931Z" }, + { url = "https://files.pythonhosted.org/packages/c3/95/edda45991f71b1ec6022c1649397c1b3d82d45cf3c6323684f9a97a4a9ce/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d595809fd266b037c22a22c2d72375c3f572d89200827ecaaa7c002b015b8a2e", size = 559904, upload-time = "2025-12-31T18:22:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d8/aaeb37ebc079ad945dd3aca3fae0f358d5c05af760305e6b6fef63d1c4c7/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6ee18383827caee641d22f60be89e0a0220c1d5a00c5e8cbb744aac1d5bc876", size = 518525, upload-time = "2025-12-31T18:22:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b5/7479aadffc83bb152884f65c8d543e61d2e95592d4ed1e902019fe5b80f2/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:276591e6828cd4dba70cdfc16450607d742c78119d46060a6cf90ef006a13c71", size = 489362, upload-time = "2025-12-31T18:22:57.322Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/4c3af4d4410aff8eef2077425406ddb20ccd3eb8b0fb5d6b6bd5fd2510a3/fastcrc-0.3.5-cp314-cp314-win32.whl", hash = "sha256:08ade4c88109a21ad098b99a9dc51bb3b689f9079828b5e390373414d0868828", size = 139114, upload-time = "2025-12-31T18:23:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/c2564781aeb0c7ae8a6554329b2f05b8a84a2376d2c0ba170ed44ddcc78c/fastcrc-0.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:01565a368ce5fe8a8578992601575313027fb16f55cf3e62c339ae89ccd6ccd2", size = 147063, upload-time = "2025-12-31T18:23:17.152Z" }, + { url = "https://files.pythonhosted.org/packages/33/79/277500a3ac37b3adf2b1c7ab59ddb72587104b7edb5d1a44f9b0a5af4704/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d169f76f8f6330ef4741eadda4cba8f0254c3ec472ed80ebb98d35fc50227d7c", size = 281969, upload-time = "2025-12-31T18:19:54.201Z" }, + { url = "https://files.pythonhosted.org/packages/08/d3/fe850eaf2e4b9be41fa4ae76c4d02bdf809a382d0d7b63cf71d84b47ecca/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d45e1940d5439f2f6fa1f8f1e4202861fb77335c7432f3fc197960af0c6f335d", size = 289745, upload-time = "2025-12-31T18:20:13.831Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/ec4f384a15a95bbad15e359d9d63294c4842eee08f5c576ee22ff3c45035/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8527fded11884789b0ecb9b7d93d9093e38dbfc47d4daefa948447e2397d10b", size = 407620, upload-time = "2025-12-31T18:20:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/98/b8/feb49bf3a2539df193481915653da52a442395c35ffeaf1deb0a649bae87/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6530a28d45ca0754bbeca3a820ae0cce3ded7f3428ed67b587d3ac8ea45fc4aa", size = 307050, upload-time = "2025-12-31T18:21:09.566Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/93fe977525ccb06a491490f53042b3682f15e626263917e3e94cd66d877a/fastcrc-0.3.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4f1229f339b32987e4ad35ae500564a782ce4e62f260150928c0233f32bb6e83", size = 290287, upload-time = "2025-12-31T18:20:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e5/0b6ae9ca6cc8ae599ca298e8a42d96febbc69b52d285464bb994f0a682ff/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7d7a56aa52c40d4293230d2751f136346d6a2b392fa2a38fe342754a6d9c238e", size = 464193, upload-time = "2025-12-31T18:22:02.338Z" }, + { url = "https://files.pythonhosted.org/packages/4a/97/7d4ed6342b244c30b81daabaa6eac591426e216455205e5c85b8566fcd19/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5816694e6aac595cca7a4331c503ed00a22889e0f97a0fa82779ed27316c90ee", size = 559728, upload-time = "2025-12-31T18:22:21.143Z" }, + { url = "https://files.pythonhosted.org/packages/eb/4e/b9cac563d0692345bc9e6dfdc7db92695dd1b3b431ac8fe61ec1dbd6d637/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b598bb000c4290e1eb847ae20a1e07f4ad064d364c2471864fa4c8ccca0f22f6", size = 518422, upload-time = "2025-12-31T18:22:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/a3/58/ef3751447b173ae84d046f90a7dac869b9ff4e11639694f60d8c04f784ea/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:56d564c7ec3dc8116c291963834f0925b4a32df1ea53d239fd470fcdde6b80e4", size = 488965, upload-time = "2025-12-31T18:22:59.006Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8b/4c32ecde6bea6486a2a5d05340e695174351ff6b06cf651a74c005f9df00/filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9", size = 40319, upload-time = "2026-03-09T19:38:47.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, +] + +[[package]] +name = "gcovr" +version = "8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorlog" }, + { name = "jinja2" }, + { name = "lxml" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/37/b4a87dff166dc0a5002e9d03fcb6ca8eeff048247b011b67f047e31122c9/gcovr-8.6.tar.gz", hash = "sha256:b2e7042abca9321cadbab8a06eb34d19f801b831557b28cdc30a029313de8b9e", size = 199997, upload-time = "2026-01-13T20:04:30.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/be/f722c843e7875c7cf92cf0e0c1604cddda55a70278c768c6327a78fdba79/gcovr-8.6-py3-none-any.whl", hash = "sha256:dbf9d87c38042752ad6f530aa8210427e22b526611bb7b7bfed0e81977d1f1ef", size = 254618, upload-time = "2026-01-13T20:04:28.15Z" }, +] + +[[package]] +name = "humanize" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, +] + +[[package]] +name = "identify" +version = "2.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "inflate64" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/f3/41bb2901543abe7aad0b0b0284ae5854bb75f848cf406bf8a046bf525f67/inflate64-1.0.4.tar.gz", hash = "sha256:b398c686960c029777afc0ed281a86f66adb956cfc3fbf6667cc6453f7b407ce", size = 902542, upload-time = "2025-11-28T10:55:52.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/33/5cfa7468960de1be0833e7e41adf5b7804a0aef2fb46f3679df3876bf3ab/inflate64-1.0.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8009e4a4918ee6c8cbc49e58fe159464895064cfdf0565fed3f49ca81e45272", size = 58619, upload-time = "2025-11-28T10:54:48.315Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0a/583c7c2832da36e986c5758d0afb6f5944599e55c5b798b066a9ef63e581/inflate64-1.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d173a7a0e865bb7d19685c5b1ad2994712b8361b24136d7e94abeff58505647", size = 35865, upload-time = "2025-11-28T10:54:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/9c6acfbe900e5c8698132244c68036b0455bd2169f46e356c83dc0366f11/inflate64-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8bad992f2d034f5f7e36208e54502d1b0829ce772c898e5dc59109833420148a", size = 36019, upload-time = "2025-11-28T10:54:50.813Z" }, + { url = "https://files.pythonhosted.org/packages/7f/66/c0c3d3b4b863aab2c2ce631d219a8eb3b95b78acd5f40d3212f071e693db/inflate64-1.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bfcf806912ced77a21394f7363805ecacd626b79f93cba87d505a48e88ede78", size = 98765, upload-time = "2025-11-28T10:54:52.273Z" }, + { url = "https://files.pythonhosted.org/packages/37/00/1a2351a85d36b26c5b2b8cfbb37ad86084c98f592dd7590f8577d8b33993/inflate64-1.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62d1aac3aba094ae42e27ce7581b414c90f218248be0953b6aeb11a127225e5d", size = 100594, upload-time = "2025-11-28T10:54:53.684Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3e/5d18ff5b86aaaf54117e1bb6ce15cb17163f56035f9c480e609d35f258ab/inflate64-1.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8065166f355122484f004225b379d403346bdae69ec624786a9334f025580675", size = 96745, upload-time = "2025-11-28T10:54:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/306d5d6aca1e04e596d2a504a59ff9a900623a6ec852f38aab99f384562d/inflate64-1.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94a95f32087d223d2e119ff5c7c264109e8d4cb7e421e7a688a899a6fe021b38", size = 99795, upload-time = "2025-11-28T10:54:56.485Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ec/d4caa4bf3c9e520c15e900fee5a00fc523953843e14aa378ca1abfb2b4ea/inflate64-1.0.4-cp312-cp312-win32.whl", hash = "sha256:ad4fa490bb7dc2a4640a3adaa2d5950f4a465ba034bbcf184c2103646e58ad97", size = 32956, upload-time = "2025-11-28T10:54:57.642Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/c0de4e9bdf12e449360b710e9ab5b5248804610f382b538773cbd07b72bb/inflate64-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:2c6befdf83d088a6e0d10d0873a9d4bfde2ce00ad7a52c8189cf303306f98030", size = 35577, upload-time = "2025-11-28T10:54:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/03a5ef74ad3869b3c5af3b09216321f5a1a5a45265f7bd6d5abc669c7622/inflate64-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:2b263c619469f90a75f29c421c53d31b208ad494a078235a8f6db2bc96583fdc", size = 33465, upload-time = "2025-11-28T10:55:00.568Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/b7de7ae318a4f233f892c4f7c8b7e0e8643abe3fdcc53ed35020a9fe3f47/inflate64-1.0.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3f37540d0e64884a935fd62a7d17e40ab69f05ec63e815483b6513675d01bef", size = 58633, upload-time = "2025-11-28T10:55:01.931Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5f/6f89c8524503fd7a9ca2bd91fe60d7291b3f684e9d41edb38ef49e10fc3d/inflate64-1.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4d24112180c95d12f279cade9a1e21f8be7f4790c4109c293292edf87d061992", size = 35864, upload-time = "2025-11-28T10:55:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/7d/14/9eadd59244b38cf85ccd0ca43d1296c50b3a33aa37be4fb68a1928efa58c/inflate64-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5c098dab17821f466fc6e6a3d78fc6e0295bb51458015f03416b1d58d6a8df4f", size = 36019, upload-time = "2025-11-28T10:55:04.126Z" }, + { url = "https://files.pythonhosted.org/packages/ca/04/399e82d8f5003dd92c8a0c5c1a9a8ce0919114710a496cbe88848bff3a72/inflate64-1.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a984b9287ff0fb596eb058d66a9e94530556afd2b7c054b44f2e0aeeff894e8f", size = 98973, upload-time = "2025-11-28T10:55:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/038dc2593bbc4272d87eac8c9f75692267d47f834ced888f6d81995df606/inflate64-1.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62a13d0327631778fa2a47c308ae2b07b2659b7bb8564783259ac65949f8c0c", size = 100777, upload-time = "2025-11-28T10:55:06.41Z" }, + { url = "https://files.pythonhosted.org/packages/74/4a/f6d3031dd3578510894a41bfe1ac149228970ce1629a43f20e0c5abbe8d1/inflate64-1.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:513201336fb3b0b7e2aee5dbbbe30a9f1b23291738b5ceb80076fc285f2ec2f1", size = 96967, upload-time = "2025-11-28T10:55:07.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/7ab3bde4e176609ae0e607a8a9bb38d201885275664a6d574299f5bf7850/inflate64-1.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84ce3a97272ba745fce52b38363855c7201968f6402a794bbade774e64c657b9", size = 99997, upload-time = "2025-11-28T10:55:08.85Z" }, + { url = "https://files.pythonhosted.org/packages/39/59/8256b3e802e203c8645e0b32b25e6bd94508ad572593f0cdf8234db3879a/inflate64-1.0.4-cp313-cp313-win32.whl", hash = "sha256:332051a9d7e50579b90a3f555d68f53414b06f636c9ffe82e97c0baae3c8fbcc", size = 32958, upload-time = "2025-11-28T10:55:10.343Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4f/5784ee1eb8260f2310e24ef2883f1f494f9332bcfde4ed14ee780372149e/inflate64-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:3983f53b590ff7d0ba243f664ce852aca882482f30f7a8eab33e10d769336d0c", size = 35578, upload-time = "2025-11-28T10:55:11.389Z" }, + { url = "https://files.pythonhosted.org/packages/39/e8/8d927770ce25dc9764c8104207a80653d65471d0a6a8f9ead350016e4586/inflate64-1.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:118d8286f085e99a14341c76ef9fbffd56619ccc80318a9a204aea3dbfa71470", size = 33465, upload-time = "2025-11-28T10:55:12.824Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fb/ec9d10f44f2fc7666ad5d70acae2b8a1941e8e08ccae1fad0820f7796be3/inflate64-1.0.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4f61925b2d4248eac2ebb15350a80aaa0d1f7f1dc770bd5ebbbb3b0db4a6a416", size = 58727, upload-time = "2025-11-28T10:55:13.834Z" }, + { url = "https://files.pythonhosted.org/packages/81/80/24ba0d2ee14e07e275e9c5b058e59a8a58f8ef42dd51a78ebbfd7c857ac4/inflate64-1.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c1acf18b08b32981a4a11ec5a112b8ad5d7c7a5b15cb5bdbdb5b19201e9aa180", size = 35945, upload-time = "2025-11-28T10:55:15.225Z" }, + { url = "https://files.pythonhosted.org/packages/70/b8/073a79716e093db973b8823bdfb02e10fbdf65642dbe1fa3cda24832aeb2/inflate64-1.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abddae8920b2eaef824254e14b8d4ff54afbe6194a1bbe9816584859f0c1244d", size = 36060, upload-time = "2025-11-28T10:55:16.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f0/87d3c317ed0acd94f5e9b3b1b9e9000228ea2af0cb4618c62cbfc816da34/inflate64-1.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b303132cc562a906543a56f35c4e164e3880da6ff041cb4a7b1df9f9d2b4bb69", size = 98995, upload-time = "2025-11-28T10:55:17.405Z" }, + { url = "https://files.pythonhosted.org/packages/90/72/0b6035302e9c33f004240a50cb6e2e1fc7bb1f2b415b02d939c551bdd06b/inflate64-1.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f0993214dea0738c557fa56c13cd9083aef0097a201d726c21984ad7f577514", size = 100781, upload-time = "2025-11-28T10:55:18.597Z" }, + { url = "https://files.pythonhosted.org/packages/2b/05/5f383c615ec0f01bcbbc699a71da167623e494083ab7ed0df86b4bddf125/inflate64-1.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a6baedc3288d7a4ff588951d3a9a97a5391dceed6255ff5b16e42cae7274bfa9", size = 97038, upload-time = "2025-11-28T10:55:20.156Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9a/c1482718c717c49c67490c42b4fdced9476e894eaebd52193e488c12e188/inflate64-1.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a846ce1f38845b20bef2625af1b512be83416d97824539524c5a34e7a729aec7", size = 100016, upload-time = "2025-11-28T10:55:21.337Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7f/700ede7474e72a1c0e2e8fe36624cd0225ca8b2875eca33d64aa2de75f4d/inflate64-1.0.4-cp314-cp314-win32.whl", hash = "sha256:eef87908c780439393d577a155868317f0a275b47b417db9f47d8633ec791745", size = 33691, upload-time = "2025-11-28T10:55:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/f2972df8cceecc9bf3afa3353d517ffc7125285198c844588e9aaf98f5d0/inflate64-1.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:fb2fdd63ef3933b67af98b3f2ee2f57e7787278041d7ba4821382fedd729b68a", size = 36304, upload-time = "2025-11-28T10:55:24.005Z" }, + { url = "https://files.pythonhosted.org/packages/29/77/16200aced67215119fb30ec9a5889b48289ee2fa5ce5137623a9ad41b2c4/inflate64-1.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:2e129669a0243ac7816fd526946ee01c25688fe81623a6d6bc95b3156d80f4fb", size = 34491, upload-time = "2025-11-28T10:55:25.068Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/b466ec7666c40912ea81a305a8a2b75f5998e6cec1d3d75e067d76203731/inflate64-1.0.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b17bf665d948dc4edeea0cd17752415d0cd7240c882b9c7e136ad4cc4321e9d4", size = 59300, upload-time = "2025-11-28T10:55:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/16e1168ac80f39894e6cb18b439eec63fec42cbced239aebfe12081b6ec7/inflate64-1.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6751758301936fbb38fa38eb5312e14e27b6a1abf568f83c17557fab2694373d", size = 36258, upload-time = "2025-11-28T10:55:27.295Z" }, + { url = "https://files.pythonhosted.org/packages/25/42/c463b42fd8a7947b4445fbaf57c265bb7f3114362fb7aee6884ffd8b5341/inflate64-1.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d6a4136752aa2a544301059d8f13780aeb88c34d60770258436a87dacd3fc304", size = 36296, upload-time = "2025-11-28T10:55:28.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/f1/cf6121926e405020e9e7bccb78ec7781fbc87500ec67368e7d9e866758be/inflate64-1.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:938ebc6b28578bfd365d1a9fdb18b7faab08321babeb2198e8025d07d8dc7fb5", size = 106788, upload-time = "2025-11-28T10:55:29.797Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dd/b653f9962497cf4d3520d69272894c37fd76f86a0e04bb3bd9f32827dc2f/inflate64-1.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61f51f80fa6f367288343c1a2cd20a42af454883087064e9274fd2a8c3a5a200", size = 107959, upload-time = "2025-11-28T10:55:31.306Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/65d88109b63611c9b6c29008201107127cf2603d186ab99beec39d85f38e/inflate64-1.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:172b51da7bbfa66b33f0a5405e944807b9949e92cf4cd9f983c07af8152766df", size = 104213, upload-time = "2025-11-28T10:55:32.506Z" }, + { url = "https://files.pythonhosted.org/packages/f9/94/c17de2f55b9fb1269bca4657f9089efe4ba0f3d4b652f07b34dfc69f69a2/inflate64-1.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ca9a2985afd5a14fb48cd126a67e5944ccb7a0a6bdec58c4f796c8c88a84539", size = 106629, upload-time = "2025-11-28T10:55:33.735Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/8bbc587bb2ad09ab7edf1f9215b2c5faf4fa7ee7c071daefe9ed55e28814/inflate64-1.0.4-cp314-cp314t-win32.whl", hash = "sha256:f8964ceaabea294bc20abc9ef408c6aae978a75c25c83168a76cd87a37c38938", size = 33865, upload-time = "2025-11-28T10:55:35.335Z" }, + { url = "https://files.pythonhosted.org/packages/91/87/8bf6f412f93c8b6dc14866a021b83321331fbdd17f6ab902a24dbf88773d/inflate64-1.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:7d13b04cba65c12d21e65eaa77da9484e265e8e821b26e0761d1455ad3a878d9", size = 36943, upload-time = "2025-11-28T10:55:36.983Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/33447bb3c4e3c0ae7b1fde3aadc52a18b2b0193cfcf4f585977e924c6463/inflate64-1.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:9ae3ee727235a06dc3cd353ee5761fdd8e3b56ad119c711f61680528972a6ced", size = 34846, upload-time = "2025-11-28T10:55:38.433Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "lsprotocol" +version = "2025.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/26/67b84e6ec1402f0e6764ef3d2a0aaf9a79522cc1d37738f4e5bb0b21521a/lsprotocol-2025.0.0.tar.gz", hash = "sha256:e879da2b9301e82cfc3e60d805630487ac2f7ab17492f4f5ba5aaba94fe56c29", size = 74896, upload-time = "2025-06-17T21:30:18.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/f0/92f2d609d6642b5f30cb50a885d2bf1483301c69d5786286500d15651ef2/lsprotocol-2025.0.0-py3-none-any.whl", hash = "sha256:f9d78f25221f2a60eaa4a96d3b4ffae011b107537facee61d3da3313880995c7", size = 76250, upload-time = "2025-06-17T21:30:19.455Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "meson" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/d3/8c43e758cf456273c32652bb8b7a4ec2d74327d8849856b0b714ad671da7/meson-1.10.1.tar.gz", hash = "sha256:c42296f12db316a4515b9375a5df330f2e751ccdd4f608430d41d7d6210e4317", size = 2413969, upload-time = "2026-01-18T14:45:08.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d5/582789135863eec7c8c1fa31fbde401b3d5d82dbbb4a0973351a1698f738/meson-1.10.1-py3-none-any.whl", hash = "sha256:fe43d1cc2e6de146fbea78f3a062194bcc0e779efc8a0f0d7c35544dfb86731f", size = 1057724, upload-time = "2026-01-18T14:45:02.584Z" }, +] + +[[package]] +name = "multivolumefile" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/f0/a7786212b5a4cb9ba05ae84a2bbd11d1d0279523aea0424b6d981d652a14/multivolumefile-0.2.3.tar.gz", hash = "sha256:a0648d0aafbc96e59198d5c17e9acad7eb531abea51035d08ce8060dcad709d6", size = 77984, upload-time = "2021-04-29T12:18:39.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/31/ec5f46fd4c83185b806aa9c736e228cb780f13990a9cf4da0beb70025fcc/multivolumefile-0.2.3-py3-none-any.whl", hash = "sha256:237f4353b60af1703087cf7725755a1f6fcaeeea48421e1896940cd1c920d678", size = 17037, upload-time = "2021-04-29T12:18:38.886Z" }, +] + +[[package]] +name = "ninja" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "patch-ng" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/bb/ebd7c6058dcfbf634986f9a8b3fb638f3269501c73701a48b7530042da5b/patch-ng-1.19.0.tar.gz", hash = "sha256:27484792f4ac1c15fe2f3e4cecf74bb9833d33b75c715b71d199f7e1e7d1f786", size = 18488, upload-time = "2025-10-08T15:17:19.616Z" } + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "py7zr" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-zstd", marker = "python_full_version < '3.14'" }, + { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, + { name = "brotlicffi", marker = "platform_python_implementation == 'PyPy'" }, + { name = "inflate64" }, + { name = "multivolumefile" }, + { name = "psutil", marker = "sys_platform != 'cygwin'" }, + { name = "pybcj" }, + { name = "pycryptodomex" }, + { name = "pyppmd" }, + { name = "texttable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/e6/01fb15361ca75ee5d01df6361825a49816a836c99980c5481da0e40c6877/py7zr-1.1.0.tar.gz", hash = "sha256:087b1a94861ad9eb4d21604f6aaa0a8986a7e00580abd79fedd6f82fecf0592c", size = 70855, upload-time = "2025-12-21T03:27:44.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/9c/762284710ead9076eeecd55fb60509c19cd1f4bea811df5f3603725b44cb/py7zr-1.1.0-py3-none-any.whl", hash = "sha256:5921bc30fb72b5453aafe3b2183664c08ef508cde2655988d5e9bd6078353ef7", size = 71257, upload-time = "2025-12-21T03:27:42.881Z" }, +] + +[[package]] +name = "pybcj" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/2670b672655b18454841b8e88f024b9159d637a4c07f6ce6db85accf8467/pybcj-1.0.7.tar.gz", hash = "sha256:72d64574069ffb0a800020668376b7ebd7adea159adbf4d35f8effc62f0daa67", size = 31282, upload-time = "2025-11-29T00:53:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/60/39b51114e3e740b61844448c3b61be146781a5c0ffabcd473a17ba7f4336/pybcj-1.0.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d39787b85678d2ab1c67e2f21dd2e71be851f08e5c9fe619c605877b57dd529d", size = 31858, upload-time = "2025-11-29T00:52:45.566Z" }, + { url = "https://files.pythonhosted.org/packages/ee/15/df4cd94bdf6a73c2b6ecf5e99dd9dcfe654215992a0114860ff1c94752b5/pybcj-1.0.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8cd5dd166093a1fb146fb78859aac0f00b45db6c11074705517bc72a940a1c8e", size = 23746, upload-time = "2025-11-29T00:52:46.947Z" }, + { url = "https://files.pythonhosted.org/packages/15/1a/f8bbe5f9ad95a0c2d1853006a93021aa1c2851b25a6bccc0894b1d72c0f4/pybcj-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82152e8641f5ce68638f3504227065f27b6b1efe96479ffbf20d81530c220062", size = 24088, upload-time = "2025-11-29T00:52:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/53675b56f9dbcb7a4dd681af8c05b1abf95fea3cdf7bf64872b9e0fdc8c8/pybcj-1.0.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2095b45d05f8d19430167b7df52ebd920df854ab8d064bae879df0a4611374b3", size = 52407, upload-time = "2025-11-29T00:52:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/a1/56/3008e748c7d35c407db97b77af52ade07756033250d0e208a6af231131ca/pybcj-1.0.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b400c9f48faed01edb7f0df54b4354270325c886e785f31c866c581a46023b", size = 51462, upload-time = "2025-11-29T00:52:51.016Z" }, + { url = "https://files.pythonhosted.org/packages/83/f4/8e8b079af7ac6a51b2edcb8bed6040a9748542cb1daf55387f769f9571d0/pybcj-1.0.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8210e51a2d4e5ccb4fdb75a1e692dd8c121858b589026bb28988ed7ffdb7ed00", size = 50320, upload-time = "2025-11-29T00:52:52.835Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/c2277eeb083029313f5822a491c39d7af91ebd1e717f42c772d56b8c3c4e/pybcj-1.0.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6c3fe420083186ae2e5f75c23aa6563dcb030b8fc188d00778ce374d1df1984", size = 49987, upload-time = "2025-11-29T00:52:53.904Z" }, + { url = "https://files.pythonhosted.org/packages/a0/52/711a94d5ae634ff3dd51324a40885158f819ba660b4601653bd78bbd33cd/pybcj-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:c435062d66364f85674a639541980000e37657b98367a2ce2699514e44b8ab05", size = 24940, upload-time = "2025-11-29T00:52:54.973Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d4/02e3ca25deca3359e63b70e5804bdbae9f400d03f93d0c66341e0471bb39/pybcj-1.0.7-cp312-cp312-win_arm64.whl", hash = "sha256:3f74fd70b08092e58b1ee13c67fbf9de63d73eb1c61ab06670a0d7161efeb252", size = 23410, upload-time = "2025-11-29T00:52:55.944Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6f/b08d5be15209b584858981b44a447ae7a6d8c591487e502e212b5420f94e/pybcj-1.0.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d5e0feeeee3a659b30d7afbd89bf41da84e8c8fe13e5b997457e799a70fa550", size = 31869, upload-time = "2025-11-29T00:52:56.973Z" }, + { url = "https://files.pythonhosted.org/packages/47/e5/68bffbc87581ea96bb4aea623d8cd085786f36d5b912ed8d9bade3265110/pybcj-1.0.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:60baaf9f0da31438515a401145f920f75f2ec7d511165bbf57475467af72a3e6", size = 23750, upload-time = "2025-11-29T00:52:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/718d6daa0d5e15e2131301220eaefbf6bc8cd0c90791c5ac18c893be111a/pybcj-1.0.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c6e726618c3d43c730df5a4067fc19653b360f89c2f72f4323dae10d324552", size = 24089, upload-time = "2025-11-29T00:52:59.457Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a6/7df6b55b64b370e2d42aa51b88402336d7f17c97eda284755f404d8d6047/pybcj-1.0.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a5fcd40a4ce8f0c5428032ec5db9f03abb42214b993886cdf558e5644de636e", size = 52454, upload-time = "2025-11-29T00:53:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/2ce282501a39f32f27cef2a3ee11c621f9d5348da4441d2326f2fcc9b17e/pybcj-1.0.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:029112255c22de66e0117bec932c8be341ed20c56dcf6a961c14689f7f0ce772", size = 51522, upload-time = "2025-11-29T00:53:01.714Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2a/f75ea5f09c91e01456a13759a325e007663855fa16af28830ce7d44d5427/pybcj-1.0.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6492bcef5cb6883506b9dce5e48cb81217407305957b0e602c6c689c60097c5e", size = 50398, upload-time = "2025-11-29T00:53:02.839Z" }, + { url = "https://files.pythonhosted.org/packages/5b/14/6d49e0c62a0ab68aa3325e6f141c33f37e5bc9d61cc9a1186c0a2d324fbe/pybcj-1.0.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22a7f4a51d36a1abb67a61e93248f997eb2be278f788d681096f5044ae18b4f9", size = 50087, upload-time = "2025-11-29T00:53:03.966Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/a0d58633236783028c08e585b3c4f4715a4286970b4ec4a25acfe23794f1/pybcj-1.0.7-cp313-cp313-win_amd64.whl", hash = "sha256:ebcce9b419fe5d3109150a1fab0fc93a64d5cd812ca44c5ddb7d4f7128ea369f", size = 24939, upload-time = "2025-11-29T00:53:05.363Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/8856cd8bc07e66322a55750ab87a264829cdfbc6cd85c5844340cf06bc53/pybcj-1.0.7-cp313-cp313-win_arm64.whl", hash = "sha256:bc6acf0320976b4e31bdc0e59b16689083d5c346a6c62ac4f799685d1cc5cf27", size = 23409, upload-time = "2025-11-29T00:53:06.749Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9e/eb50f11ea7fb6342167bb8353d48966b490afa8ae47c98917e9acd045b71/pybcj-1.0.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:293f951eb3877840acab79f0c4dcfc06eab03e087cb9e4c004ec058e093acb1d", size = 32409, upload-time = "2025-11-29T00:53:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f6/b55f4a5faf9bd162426f49de84873fbf813698d1b018234c3c99816a9662/pybcj-1.0.7-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3ae64960904f362d33ffca10715803afd9f9a6a2a592f871dcb335acf82edf29", size = 23818, upload-time = "2025-11-29T00:53:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/4c/24/78cf0973b3deded1e072479ac35d387083a626b3dc0b29e2e099cf73e082/pybcj-1.0.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:70aa4476910f982025f878e598c136559a6d78b59fc20ba8b4b592306cde6051", size = 24115, upload-time = "2025-11-29T00:53:10.241Z" }, + { url = "https://files.pythonhosted.org/packages/e6/69/e044d7127018158c67b119e6ed26d1a4b34b1e1defd9b0d3de029a782b9f/pybcj-1.0.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79ce3ce9b380b1b75c5e490abc3888ee3b5b2d28c22b59618674bf410b9cee16", size = 52484, upload-time = "2025-11-29T00:53:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/98/05/55337b0a9807887967b2cf49dd6f3bf47c7745786b0bda51a5cbfda66c78/pybcj-1.0.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc73ee1bc064d6f97dfd66051d3859b32e1b6a4cf89b077f5c8ef6c2dccb71af", size = 51471, upload-time = "2025-11-29T00:53:12.86Z" }, + { url = "https://files.pythonhosted.org/packages/59/09/6c57cbbf4931ac304f822ef78f478dd6b34b00b08e9530d308737959f064/pybcj-1.0.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5b0d13f41a9f85b3f95dd5dc7bfaa9539e80f8ae60a96db7f34c07ed732e4a82", size = 50435, upload-time = "2025-11-29T00:53:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/26415ca1b96456e27550dae67092023af3e8454621b1efa701079d8acb64/pybcj-1.0.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:597d7e9a8cbb30a6ed54d552fd3436edb32bbb821a7ac2fa8e5c7ebd1f7e0e93", size = 50010, upload-time = "2025-11-29T00:53:15.396Z" }, + { url = "https://files.pythonhosted.org/packages/91/f7/d4e55aede85143adff3ded89a1ae87bbf29060ddc2249fb861d78b103d4a/pybcj-1.0.7-cp314-cp314-win_amd64.whl", hash = "sha256:4603cc41ceb1236abe9169e2ead344140be5d2c3ac01bbc5e44cb1b13078a009", size = 25283, upload-time = "2025-11-29T00:53:16.472Z" }, + { url = "https://files.pythonhosted.org/packages/f6/19/9939ba437140d7375aecf8063d8695dfeea56ca40f57ddaa6ca3b828c131/pybcj-1.0.7-cp314-cp314-win_arm64.whl", hash = "sha256:adf985e816ddd59f3bf6d1066b7fa89de7424a4f19f3725f9976284cabe54e28", size = 23622, upload-time = "2025-11-29T00:53:17.994Z" }, + { url = "https://files.pythonhosted.org/packages/62/6e/5e14c70f3ddc268f28e7ac912510c8bc2b5430f18bb7f326bbb9d1878955/pybcj-1.0.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bbd835873de147481d62c11ba91a75d26a72df1142de3516b384b04e5a1db6d", size = 33016, upload-time = "2025-11-29T00:53:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/49/c1/25a1a8d5811c5d2a2ca3439c548abbba882fa72eba9a6eb41040206fdc26/pybcj-1.0.7-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b7576b25d7b01a953e2f987e77cef93c001db7b95924a5541d5a55f9195a7e89", size = 24114, upload-time = "2025-11-29T00:53:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/35/66/09567564cdc6cc2713d729bd29794367ebb67fa9ea5c10313c49608c08fa/pybcj-1.0.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57b36920498f82ca6a325a98b13e0fbff8fc29bade7aaaddc7d284640bffd87d", size = 24428, upload-time = "2025-11-29T00:53:21.522Z" }, + { url = "https://files.pythonhosted.org/packages/c5/49/4f6793624eb418cc2536ef39e67c8783d7ca542f6139a051e9d2d76fdc80/pybcj-1.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aac2a46faf41e373939f6d3e6a5aa2121bf09e2446972c14a8e5d1ca3b0f8130", size = 58806, upload-time = "2025-11-29T00:53:22.568Z" }, + { url = "https://files.pythonhosted.org/packages/be/1c/8604f7fe360a9340bbf798b826a88f8e9d186fc031eb531bcd11ac9e684b/pybcj-1.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c7d6156ef2b4e8ecd450b62dc4cc3a89e8dda307cb26288b670952ef0df3a37", size = 56975, upload-time = "2025-11-29T00:53:23.691Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/d706b4220f60fa4e6a5756dc0051fb76c32a5615958fc12f0dcacef3f86b/pybcj-1.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0fe306213de1e764abae63c06ae5a4e9a83632f62612805f1f883b8d74431901", size = 56023, upload-time = "2025-11-29T00:53:25.135Z" }, + { url = "https://files.pythonhosted.org/packages/3e/28/ad306f5acb1226241960bb86e9021b4e32bb7da426ccdc824d9d36d61261/pybcj-1.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00448182d535cca37e8f24d892d480fa86f80ff20c79385f6eca75f118efcbb4", size = 55194, upload-time = "2025-11-29T00:53:26.209Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d8/70c5e5701926fc67dae02101f0298d21c0b89ff83fa705712c3b4da252e5/pybcj-1.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:7e94aa712d0fa5fda9875828441755ece7121fc3f8c5cc3bc8ee92d05b853590", size = 25826, upload-time = "2025-11-29T00:53:27.655Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5d/7a87ba32c0c0756f36000fafe642fa4609be2c26a50a7913a057a47eabdf/pybcj-1.0.7-cp314-cp314t-win_arm64.whl", hash = "sha256:16fd4e51a5556d1f38d7ba5d1fab588bfb60ae23d2299b5179779bf9900adf71", size = 24049, upload-time = "2025-11-29T00:53:28.679Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886", size = 2498764, upload-time = "2025-05-17T17:22:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d", size = 1643012, upload-time = "2025-05-17T17:22:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa", size = 2187643, upload-time = "2025-05-17T17:22:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8", size = 2273762, upload-time = "2025-05-17T17:22:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5", size = 2313012, upload-time = "2025-05-17T17:22:30.57Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314", size = 2186856, upload-time = "2025-05-17T17:22:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006", size = 2347523, upload-time = "2025-05-17T17:22:35.386Z" }, + { url = "https://files.pythonhosted.org/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462", size = 2272825, upload-time = "2025-05-17T17:22:37.632Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328", size = 1772078, upload-time = "2025-05-17T17:22:40Z" }, + { url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708", size = 1803656, upload-time = "2025-05-17T17:22:42.139Z" }, + { url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4", size = 1707172, upload-time = "2025-05-17T17:22:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z" }, + { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, +] + +[[package]] +name = "pygls" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "lsprotocol" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/94/cce3560f6c7296be43bd67ba342f8972b8adddfe407b62b25d1fb90c514b/pygls-2.0.1.tar.gz", hash = "sha256:2f774a669fbe2ece977d302786f01f9b0c5df7d0204ea0fa371ecb08288d6b86", size = 55796, upload-time = "2026-01-26T23:04:33.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/8b/d0d7e51f928ec6e555c943f7c07a712b992e9cf061d52cbf37fbf8622c05/pygls-2.0.1-py3-none-any.whl", hash = "sha256:d29748042cea5bedc98285eb3e2c0c60bf3fc73786319519001bf72bbe8f36cc", size = 69536, upload-time = "2026-01-26T23:04:35.197Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymavlink" +version = "2.4.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastcrc" }, + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a2/0a4ce323178f60f869e42a2ed3844bead7b685807674bef966a39661606e/pymavlink-2.4.49.tar.gz", hash = "sha256:d7cf10d5592d038a18aa972711177ebb88be2143efcc258df630b0513e9da2c2", size = 6172115, upload-time = "2025-08-01T23:33:10.372Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/81/062427da96311d359ed799af8569b7b3ffa25c333fb4a961478ce5a4735f/pymavlink-2.4.49-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b7925330a4bb30bcc732971cfeb1aa54515efd28f4588d7abc942967d7a2298b", size = 6291309, upload-time = "2025-08-01T23:32:04.972Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/8bfed758e2efc2d6f28259634d94c09b10e50c62cd5914ac888ce268378d/pymavlink-2.4.49-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf4c13703bc6dcbc70083a12aaec71f3a36a6b607290e93f59f2b004ebd02784", size = 6226353, upload-time = "2025-08-01T23:32:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/78419c2ae5489fdd996f6af0c1e4bd6dceaa5a5b155a367851926da7b05f/pymavlink-2.4.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8522d652fef8fb03c7ee50abd2686ffe0262cbec06136ae230f3a88cccdff21c", size = 6222943, upload-time = "2025-08-01T23:32:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e6/c9ae05436ed219bb9f2b505d7c82474173c8ebcd28ff8f55833213d732a2/pymavlink-2.4.49-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1b6e29972792a1da3fafde911355631b8a62735297a2f3c5209aa985919917a", size = 6376049, upload-time = "2025-08-01T23:32:08.949Z" }, + { url = "https://files.pythonhosted.org/packages/36/6f/eb93cc44e2653044eb5bbfa7ce0f808611e42d56106a4d6d5de4db8bb211/pymavlink-2.4.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8b13b9ac195e9fa8f01cda21638e26af9c5a90e3475ddb43fd2b9e396913f6b", size = 6388174, upload-time = "2025-08-01T23:32:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/34099ab9e4e41db4b2ec9f05c3d8e7726ef3d5a2ae8cfb6f90596c4d82fb/pymavlink-2.4.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a87b508a9e9215afdb809189224731b4b34153f3879226fd94b8f485ac626ab", size = 6390472, upload-time = "2025-08-01T23:32:11.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/51/0146c0008feb5d8a7721870489b4c19fd30a1e49433be7a83624dc961f90/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31ca1c1e60a21f240abf35258df30e7b5ee954a055bbe7584f0ebabb48dd8c40", size = 6376189, upload-time = "2025-08-01T23:32:12.921Z" }, + { url = "https://files.pythonhosted.org/packages/c4/51/aa4b51cd9948eca7b63359ad392d8cd69b393bd781830c4a518a98aede33/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f854d1d730f40d4efa52d8901413af1b23d16187e941b76d55f0dcc0208d641d", size = 6378697, upload-time = "2025-08-01T23:32:14.471Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b6/dec8f9f7e1769894b7b11c8900b0a13cf13fb9cee2c45d7f9f5a785b3f39/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16c915365a21b7734c794ba97fa804ae6db52411bf62d21b877a51df2183dfab", size = 6384644, upload-time = "2025-08-01T23:32:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2e/3db53612dab0bfa31eca8e9162489f44c9f9e23c183a2b263d707eb5ddc7/pymavlink-2.4.49-cp312-cp312-win32.whl", hash = "sha256:af7e84aec82f00fd574c2a0dbe11fb1a4c3cbf26f294ca0ef3807dcc5670567e", size = 6230813, upload-time = "2025-08-01T23:32:17.732Z" }, + { url = "https://files.pythonhosted.org/packages/bf/47/fe857933a464b5a07bf72e2a1d2e92a87ad9d96915f48f86c9475333a63d/pymavlink-2.4.49-cp312-cp312-win_amd64.whl", hash = "sha256:246e227ca8535de98a4b93876a14b9ced7bfc82c70458e480725a715aa6b6bf3", size = 6242451, upload-time = "2025-08-01T23:32:19.063Z" }, + { url = "https://files.pythonhosted.org/packages/25/ca/995d1201925ad49fb6b174a9d488f1d90b77256b1088ebd3d7f192b0f65a/pymavlink-2.4.49-cp312-cp312-win_arm64.whl", hash = "sha256:c7415592166d9cbd4434775828b00c71bebf292c8367744d861e3ccd2dab9f3e", size = 6231742, upload-time = "2025-08-01T23:32:20.707Z" }, + { url = "https://files.pythonhosted.org/packages/26/10/67756d987b1aefd991664ce0a996ee3bf69ed7aaf8c7319ff6012a4dc8a2/pymavlink-2.4.49-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f7cfaf3cc1abd611c0757d4b7e56eaf5b4cfa54510a3178b26ebbd9d3443b9d7", size = 6290269, upload-time = "2025-08-01T23:32:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/c2/02/9e63467d65da78fed03981c86e5b7877fcf163a98372ba5ef03015e3798c/pymavlink-2.4.49-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db9c0d00e79946ecf1ac89847f32712ef546994342f44b3e9a68e59cfbc85bef", size = 6225761, upload-time = "2025-08-01T23:32:23.419Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7e/46a5512964043ada02914657610c885b083375dd169dea172870f4dd73b0/pymavlink-2.4.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37937d5dfd2ddc2a64ea64687380278ac9c49e1644ea125f1e8a5caf4e1f2ebd", size = 6222450, upload-time = "2025-08-01T23:32:24.803Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/1b63a8c4d35887edc979805b324240ff4b847e9d912b323d71613e8f1971/pymavlink-2.4.49-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8100f2f1f53b094611531df2cfb25f1c8e8fdee01f095eb8ee18976994663cf6", size = 6368072, upload-time = "2025-08-01T23:32:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/29/69/94348757424a94c5a3e87f41d4c05a168bc5de2549afdbea1d4424a318dc/pymavlink-2.4.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2db4b88f38aa1ba4c0653a8c5938364bfe78a008e8d02627534015142bf774", size = 6379869, upload-time = "2025-08-01T23:32:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/91/a7/792925eadc046ae580ab444181a06e8d51d38204a81a9274460f90009b88/pymavlink-2.4.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7fe9286fd5b2db05277d30d1ea6b9b3a9ea010a99aff04d451705cc4be6a7e6", size = 6382786, upload-time = "2025-08-01T23:32:28.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/d7b1d280dda800ac386fd54dcded6344b518a8266a918729512e46e39f6b/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d49309e00d4d434f2e414c166b18ef18496987a13a613864f89a19ca190ef0d0", size = 6368732, upload-time = "2025-08-01T23:32:29.794Z" }, + { url = "https://files.pythonhosted.org/packages/23/89/b75ef8eea1e31ec07f13fe71883b08cdc2bce0c33418218cebb03e55124a/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7104eef554b01d6c180e1a532dc494c4b1d74e48b0b725328ec39f042982e172", size = 6370950, upload-time = "2025-08-01T23:32:31.041Z" }, + { url = "https://files.pythonhosted.org/packages/f6/57/3cb77e3f593e27dc63bd74357b3c3b57075af74771c4446275097f0865f2/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:795e6628f9ecf0b06e3b7b65f8fcf477ec1603971d590cffd4640cff1852da23", size = 6376423, upload-time = "2025-08-01T23:32:32.345Z" }, + { url = "https://files.pythonhosted.org/packages/41/bb/49b83c6d212751c88a29cebe413c940ee1d0b7991a667710689eb0cd648e/pymavlink-2.4.49-cp313-cp313-win32.whl", hash = "sha256:9f14bbe1ce3d5c0af4994f0f76d1a8d0c2f915d7dcb7645c1ecba42eeff89536", size = 6230635, upload-time = "2025-08-01T23:32:33.613Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c4/d3e9e414dd7ba0124ef07d33d9492cc01db1b76ae3cec45443ec4d6a7935/pymavlink-2.4.49-cp313-cp313-win_amd64.whl", hash = "sha256:9777a0375ebcda0efda3f4eae6d8d2e5ce6de8e26c2f0ac7be1a016d0d386b82", size = 6242260, upload-time = "2025-08-01T23:32:35.256Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/52616b4fdd076177f1ba22e6ef40782b48e14efb47fce2c3bd4f8496ec23/pymavlink-2.4.49-cp313-cp313-win_arm64.whl", hash = "sha256:712ee4240a9489c6dab6158882c7e1f37516c5951db5841cd408ad7b4c6db0d4", size = 6231575, upload-time = "2025-08-01T23:32:36.845Z" }, +] + +[[package]] +name = "pyppmd" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/d7/803232913cab9163a1a97ecf2236cd7135903c46ac8d49613448d88e8759/pyppmd-1.3.1.tar.gz", hash = "sha256:ced527f08ade4408c1bfc5264e9f97ffac8d221c9d13eca4f35ec1ec0c7b6b2e", size = 1351815, upload-time = "2025-11-27T22:08:44.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/89/696046e53c7aea98bb563aed3f15c3e2fa20c33e3d6c9de3c20992c586cc/pyppmd-1.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3faa58ab2ebe3b13ec23b1904639d687fb727270d2962fd2d239ca00fd6eb865", size = 77796, upload-time = "2025-11-27T22:07:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/a708cdb58e76889bc65201eda12211486548ffe00ecddc5dfbdce6d4d252/pyppmd-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27703f041ee96912a5410fd3ce31c5cde32f9323bd67f72f100bd960ee67bf13", size = 48185, upload-time = "2025-11-27T22:07:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/f299535c18009addb866d89a7044f1086e4eecf50542e57ff5bb15840195/pyppmd-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e773d8353b36f7e7973a43526993fb276b98a97839cb5dc8f4e6465ad873f41a", size = 48496, upload-time = "2025-11-27T22:07:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9d/4a59b73ea8e305f9192ee26ceb7c3d57e17ccb9bcca0e99ef335db29fcf7/pyppmd-1.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37b1883accf840cb0b711785d353f8548853a1401d381da007c0aec362f3ffac", size = 142620, upload-time = "2025-11-27T22:07:54.411Z" }, + { url = "https://files.pythonhosted.org/packages/88/d7/fe32c2a4f8539365e6292aed25545830a5e718a510cdb4caddd6fd8d8056/pyppmd-1.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bd6d179ad39b6191ca0cbe62fb9592f33f49277b4384ad7bc5eb0e6ca27ebee", size = 144306, upload-time = "2025-11-27T22:07:55.727Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/e3907cf263b58f4ed4c5315bc1ac91721c85332fd0d73a89e3e2752904ef/pyppmd-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:806cf8d33606e44bf5ff5786c57891f57993f1eef1c763da3c58ea97de3a13c8", size = 139522, upload-time = "2025-11-27T22:07:57.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/22d5f93251e481f23bf741dde7f59cfc3e315f60b32b63f215a2d7bb8944/pyppmd-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1826cbce9a2c944aa08df79310a7e6d4a61fd20636b6dff64a77ea4bc43da30f", size = 142340, upload-time = "2025-11-27T22:07:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/34/88/38d36c1394d5e331db9708dae08863c87c059bd5b6e43b20234be02a0503/pyppmd-1.3.1-cp312-cp312-win32.whl", hash = "sha256:d3ff96671319318d941dd34300d641745048e8a3251b077bddf98652d6ddc513", size = 42102, upload-time = "2025-11-27T22:08:00.124Z" }, + { url = "https://files.pythonhosted.org/packages/ee/29/1398c6f67dfb66367babeed2980caee837d0a488705e3dff7ff159e017e7/pyppmd-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:c8c1ad39e7ebde71bf5a54cf61f489bf4790f1dd0beb70dc2e8f5ad3329d7ca7", size = 46957, upload-time = "2025-11-27T22:08:01.587Z" }, + { url = "https://files.pythonhosted.org/packages/d2/41/ee3193c16472a5a1c5f1965abb8fa87a7ad455c39688e9a0c2d87d6a7027/pyppmd-1.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:391b2bf76d7dc45b343781754d0b734dcbf539b92667986a343f5488c4bf9ca0", size = 45214, upload-time = "2025-11-27T22:08:02.721Z" }, + { url = "https://files.pythonhosted.org/packages/f0/01/7cc3854b0e1304b90d2435e946db5e1c42e103adf840a68400002fc838b4/pyppmd-1.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4b4edb3e9619fd0bc39c1a07eb03e8731db833a93b23134f36c7ef581a94b37a", size = 77800, upload-time = "2025-11-27T22:08:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/caf6305224b9432ea7373670d101392f620c1ce741d6f95458b1fb9a16a4/pyppmd-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8b5c813e462c91048b88e2adfbcc0c69f2c905f70097001d32066f86f675bd4", size = 48195, upload-time = "2025-11-27T22:08:05.323Z" }, + { url = "https://files.pythonhosted.org/packages/45/82/8c5d3f0f738a3cf5c209d1471efda105a16417c0aaa91a2ad39efb3d4efb/pyppmd-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e8d372d9fac382183e0371cf0c2d736b494b1857a1befe98d563342b1205265b", size = 48482, upload-time = "2025-11-27T22:08:06.441Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/09ab8b4f00473f210284dacf837cae8355ad900f3fd8fd98b2bba12de4fa/pyppmd-1.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b765ae21f7ed2f4ea8f32bfd9e3a4a8d738e73fc8f8dcddec9cbe2c898d60be", size = 142828, upload-time = "2025-11-27T22:08:07.611Z" }, + { url = "https://files.pythonhosted.org/packages/64/fd/673d429df719affa1445611288aab6111c0bc87e51eb4677fe2421a1dd64/pyppmd-1.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd00522ddfcc292304577386b6c217758c0c10e1fb9ce7877ad7d3b7b821a808", size = 144502, upload-time = "2025-11-27T22:08:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/cf/0f/64dad213f56eaf23210ac7d3a5dedb19ad8f186f8b8682f65e7d51d9fa4a/pyppmd-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3de62099ff2ca876c2d39bc547bcba6f7b878988663abd782a5bad4edac3bb44", size = 139718, upload-time = "2025-11-27T22:08:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/74/61/bd5dbe8d7374748687d48f0bc44e6b930470e09552b1bbc65ef899ab673e/pyppmd-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:011f845de195d60fe973a635a1f4be981b7d80f357a8acb1b2d83bdf5087c808", size = 142578, upload-time = "2025-11-27T22:08:11.388Z" }, + { url = "https://files.pythonhosted.org/packages/42/ae/0af00a9b1cd59b6821eb182413033d5ddbe05dfcde7511e3a6b1a5b83f6d/pyppmd-1.3.1-cp313-cp313-win32.whl", hash = "sha256:7d61bd01f25289b6ae54832db4254602fb0c6d105f6e6bf0aee39b803b698b98", size = 42102, upload-time = "2025-11-27T22:08:13.297Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6d/311d8faca142f39b4d158c94ed7eb237a197d41dd7eb67f53442a7904e04/pyppmd-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:df8d84ab72381058a964ba66e5e81ed52dbd0b5ad734a5ef8353452983506098", size = 46962, upload-time = "2025-11-27T22:08:14.759Z" }, + { url = "https://files.pythonhosted.org/packages/bb/42/24b59b6bb73a3fec96c7f81521d146c2ac89ffcb89cf9e848f62e0660381/pyppmd-1.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:124a04aab6936ba011f9ad57067798c7f052fdb1848b0cc4318606eea55475e6", size = 45209, upload-time = "2025-11-27T22:08:15.933Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/ac2a011af1c7c40b6482e60d85d267ac5923fb8794b51bfddbb56b04d1ec/pyppmd-1.3.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ea53f71ac16e113599b8441a9d8b6dcd71cfdf15cdb33ba5151810b8e656c5ec", size = 77897, upload-time = "2025-11-27T22:08:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/20/13/737f4dac06685865d23f51b7061dbe9373c7bcc60b5b6456cd08301bc807/pyppmd-1.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:985c8703b53e5f68fe17f653e96748d60b1f855676c852a6e67cd472eb853671", size = 48268, upload-time = "2025-11-27T22:08:18.263Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d3/c50ebaee8b8a064fe9a534e98df5051e309efb705728aadff4e6893cd87f/pyppmd-1.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:33daa996ad5203c665c0b55aff6329817b2cb7fa95f2c33a2e83ed0121b400cb", size = 48521, upload-time = "2025-11-27T22:08:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/80/6c/4a97c0a0ab7640aeddde4843cb6381b80ecb71fe2c6c48b9032d60586b11/pyppmd-1.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b49870c6d7194f6eb80f30335ca03596d153e02fcde2c222e4f1202ac25f7fcf", size = 142892, upload-time = "2025-11-27T22:08:20.972Z" }, + { url = "https://files.pythonhosted.org/packages/8f/22/744c32c7da3de171d9e62a1b2cb4104544ac778358565a3dbc06a2253324/pyppmd-1.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:385e92c97c42e8a6f0bfc0e4acfc6c074cb1ba3a2f650f292696dd9f19e2e603", size = 144550, upload-time = "2025-11-27T22:08:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/84af6808e0754923062122d3ee9f0532050f1612069d558bb5d53978e67a/pyppmd-1.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:017a1e2903f1c3147a1046db486990d401e8a25eb52c320b1fc2fb3e7b83cbeb", size = 139850, upload-time = "2025-11-27T22:08:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/20/5e/dc6166ea7929d442625301289d99313a5b358e3cb2e927a6bd913047e8ee/pyppmd-1.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f2770a4b777c0c5236b3d9294b7bf4bc15538c95d45b2079eb8ebc1298e62e37", size = 142589, upload-time = "2025-11-27T22:08:25.228Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/adc6b12ddf11173a02fd631010f88ad4fe4c532363959638d5d53583a3d9/pyppmd-1.3.1-cp314-cp314-win32.whl", hash = "sha256:b9d54cd59ce97f2ba57be1da91b3d874d129faca21c9565d7afec111f942e6a1", size = 42761, upload-time = "2025-11-27T22:08:26.917Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/5003a413d9f2743298a070df6713a75dedccc470e7adeeced5b43cad7418/pyppmd-1.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0e64247618cb150d2909beb0137da3084fef1d3479b4cc73b5b47fda7611abf9", size = 47806, upload-time = "2025-11-27T22:08:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/07/b7/db927e1df7fc3132cb57960f4ea23bf174c7e86ccec22857e6a35cccdae7/pyppmd-1.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:d354d6e551d2630b0ac98f27e3ad63e86cdcac9ce2115b5dfe46e2c9d3f4e82c", size = 46122, upload-time = "2025-11-27T22:08:29.191Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/3150227624f374b06e331a7f67ae3383e796dd90973ee17ec3e35d30524c/pyppmd-1.3.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2d77ed79662c32e2551748d59763cfe3dcd10855bf3495937e3d5e5917507818", size = 78876, upload-time = "2025-11-27T22:08:30.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/11a3bba62d1d92050f4dd86db810062b506ec76d20b6e00b4ab567ff15e4/pyppmd-1.3.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6a1f92b94635c23d85270bb26db25cc0db544e436af86efc1cf58302d71d5af1", size = 48830, upload-time = "2025-11-27T22:08:32.087Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/e8f50cf89c689543d5a16c7be3df3c4afd43cbeb1a019b84ff9e82f13415/pyppmd-1.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:610f214f2405e27eb5a3dad7fa15f385cfc42141a01cda71995d9c1e0b09fab9", size = 48956, upload-time = "2025-11-27T22:08:33.567Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0f/944b30679a8dea3ea95a66531ee30cb6feb5d011ec7fd6832069d9130bf3/pyppmd-1.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae81a14895498d9a23429d92114c98da478b74b8e33251527d7cff3e01c09de0", size = 152872, upload-time = "2025-11-27T22:08:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/ae/87/1e48ea92994f4c72dc9b5520a6a386b21c524d3d2969c2c2a5c325929ad7/pyppmd-1.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1683e6d1ba09e377e0ae02de3a518191a3d63ccdb0b6037c74e6ddf577b5644", size = 154210, upload-time = "2025-11-27T22:08:36.368Z" }, + { url = "https://files.pythonhosted.org/packages/3f/21/650911f98c4cb360442724bbdade27cb3679b0587ea77e73512693d2918d/pyppmd-1.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e4d74fa0f3531e9dadc56e0ace41bce82d3c0babed47b3f224101dc0dbde7287", size = 147636, upload-time = "2025-11-27T22:08:37.657Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/4f8cfc1dd67b04ced8c10669fa14c4e35a42cf4a84e1101793735a82d5f0/pyppmd-1.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f8d6375b18b9c79127fee0885cfd52e2e983edb67041464309426571d38dcd4", size = 150998, upload-time = "2025-11-27T22:08:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/c4/da/dc9e5e10bc56056bd8c33e533517cb327752cbaf172688bba3c23f6b2318/pyppmd-1.3.1-cp314-cp314t-win32.whl", hash = "sha256:de87f7acd575fb07a4ff42d41bcc071570fe759a36f345f1f54f574ecfccfc5b", size = 43016, upload-time = "2025-11-27T22:08:40.234Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a0/46dc15549ad2c0427a9825730e6bdf342045ad410543368afd4389a16f36/pyppmd-1.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:76e4800aa67292b4cc80058fd29b39e02a5dded721af9fe5654f356ef24307f4", size = 48636, upload-time = "2025-11-27T22:08:41.45Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7b/7e9a83848de928c26f0ab3ee105c0da63a932a0804a94807205e6313e73a/pyppmd-1.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:e066cbf1d335fe20480cd8ecc10848ba78d99fe6d1e44ea00def48feaf46afdf", size = 46402, upload-time = "2025-11-27T22:08:42.804Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/7e/9f3b0dd3a074a6c3e1e79f35e465b1f2ee4b262d619de00cfce523cc9b24/python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5", size = 56945, upload-time = "2026-03-10T15:08:15.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/80/73211fc5bfbfc562369b4aa61dc1e4bf07dc7b34df7b317e4539316b809c/python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e", size = 31485, upload-time = "2026-03-10T15:08:13.06Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "qgc-tools" +version = "0.0.0" +source = { virtual = "." } + +[package.optional-dependencies] +ci = [ + { name = "meson" }, + { name = "ninja" }, + { name = "pre-commit" }, +] +coverage = [ + { name = "gcovr" }, +] +dev = [ + { name = "jinja2" }, + { name = "pymavlink" }, + { name = "pyyaml" }, +] +lsp = [ + { name = "lsprotocol" }, + { name = "pygls" }, +] +precommit = [ + { name = "pre-commit" }, +] +qt = [ + { name = "aqtinstall" }, +] +scripts = [ + { name = "defusedxml" }, +] +test = [ + { name = "jinja2" }, + { name = "pytest" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "aqtinstall", marker = "extra == 'qt'" }, + { name = "defusedxml", marker = "extra == 'scripts'", specifier = ">=0.7.1" }, + { name = "gcovr", marker = "extra == 'coverage'" }, + { name = "jinja2", marker = "extra == 'dev'" }, + { name = "jinja2", marker = "extra == 'test'" }, + { name = "lsprotocol", marker = "extra == 'lsp'" }, + { name = "meson", marker = "extra == 'ci'" }, + { name = "ninja", marker = "extra == 'ci'" }, + { name = "pre-commit", marker = "extra == 'ci'" }, + { name = "pre-commit", marker = "extra == 'precommit'" }, + { name = "pygls", marker = "extra == 'lsp'" }, + { name = "pymavlink", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "pyyaml", marker = "extra == 'dev'" }, + { name = "pyyaml", marker = "extra == 'test'" }, +] +provides-extras = ["scripts", "precommit", "test", "ci", "qt", "coverage", "dev", "lsp"] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "texttable" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/dc/0aff23d6036a4d3bf4f1d8c8204c5c79c4437e25e0ae94ffe4bbb55ee3c2/texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638", size = 12831, upload-time = "2023-10-03T09:48:12.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/99/4772b8e00a136f3e01236de33b0efda31ee7077203ba5967fcc76da94d65/texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917", size = 10768, upload-time = "2023-10-03T09:48:10.434Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, +] From 5066dc09ad75bfe9032daaae9a026af46de81d04 Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Wed, 11 Mar 2026 09:25:20 -0400 Subject: [PATCH 066/119] Refactor GStreamer CMake: rename find module, fix bugs, add helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename FindGStreamer → FindQGCGStreamer to avoid conflicts with CMake's built-in FindGStreamer; update all call sites - Fix GL component MATCHES false-positive (GlPrototypes/GlEgl/GlWayland were matching the GL branch); use STREQUAL instead - Fix plugin compile definitions: GST_PLUGIN_${name}_FOUND (not GStreamer_${name}_FOUND which is for components) - Add iOS auto-download: pkg download, pkgutil expand, stale cache detection, framework location, pkg-config isolation - Refactor Android download from ~120 lines of hand-rolled logic to helper function calls (gstreamer_resilient_download, etc.) - Add Windows ARM64 SDK discovery (env vars + default paths) - Add Windows/macOS pkg-config isolation (PKG_CONFIG_LIBDIR) - Add INACTIVITY_TIMEOUT support to gstreamer_resilient_download - Move plugin/deps lists outside static-libs guard (install helpers need them for dynamic builds too) - Remove QGC_CUSTOM_GST_PACKAGE option; version comes from config - Add version validation and FATAL_ERROR for bad GStreamer_ROOT_DIR - Clean up section banner comments throughout CMake files --- .github/build-config.json | 5 +- cmake/BuildConfig.cmake | 21 +- cmake/CustomOptions.cmake | 1 - cmake/find-modules/FindGStreamer.cmake | 672 ------------------ cmake/find-modules/FindQGCGStreamer.cmake | 592 +++++++++++++++ cmake/find-modules/GStreamerHelpers.cmake | 410 +++++++++++ cmake/find-modules/_FindGStreamer.cmake | 456 ------------ cmake/find-modules/_FindGStreamerMobile.cmake | 607 ---------------- deploy/docker/entrypoint.sh | 1 - .../VideoReceiver/GStreamer/CMakeLists.txt | 148 ++-- .../VideoReceiver/GStreamer/gst_ios_init.h | 10 - .../VideoReceiver/GStreamer/gst_ios_init.m | 80 --- .../GStreamer/gstqml6gl/CMakeLists.txt | 212 ++++-- tools/setup/build-gstreamer.py | 4 +- tools/setup/resolve_gstreamer_config.py | 3 +- 15 files changed, 1240 insertions(+), 1982 deletions(-) delete mode 100644 cmake/find-modules/FindGStreamer.cmake create mode 100644 cmake/find-modules/FindQGCGStreamer.cmake create mode 100644 cmake/find-modules/GStreamerHelpers.cmake delete mode 100644 cmake/find-modules/_FindGStreamer.cmake delete mode 100644 cmake/find-modules/_FindGStreamerMobile.cmake delete mode 100644 src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.h delete mode 100644 src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.m diff --git a/.github/build-config.json b/.github/build-config.json index ea51b1b24266..e87c5c7a254b 100644 --- a/.github/build-config.json +++ b/.github/build-config.json @@ -6,9 +6,10 @@ "android_platform": "35", "cmake_minimum_version": "3.25", "gstreamer_android_version": "1.22.12", + "gstreamer_ios_version": "1.24.13", "gstreamer_macos_version": "1.24.13", - "gstreamer_minimum_version": "1.22.12", - "gstreamer_version": "1.24.13", + "gstreamer_minimum_version": "1.20", + "gstreamer_default_version": "1.24.13", "gstreamer_windows_version": "1.22.12", "ios_deployment_target": "14.0", "java_version": "17", diff --git a/cmake/BuildConfig.cmake b/cmake/BuildConfig.cmake index 789d94ad7203..e5b9a553f732 100644 --- a/cmake/BuildConfig.cmake +++ b/cmake/BuildConfig.cmake @@ -22,8 +22,10 @@ endfunction() qgc_config_get_value(QGC_CONFIG_QT_VERSION "qt_version") qgc_config_get_value(QGC_CONFIG_QT_MINIMUM_VERSION "qt_minimum_version") -qgc_config_get_value(QGC_CONFIG_GSTREAMER_VERSION "gstreamer_version") +qgc_config_get_value(QGC_CONFIG_GSTREAMER_VERSION "gstreamer_default_version") +qgc_config_get_value(QGC_CONFIG_GSTREAMER_MIN_VERSION "gstreamer_minimum_version") qgc_config_get_value(QGC_CONFIG_GSTREAMER_ANDROID_VERSION "gstreamer_android_version") +qgc_config_get_value(QGC_CONFIG_GSTREAMER_IOS_VERSION "gstreamer_ios_version") qgc_config_get_value(QGC_CONFIG_GSTREAMER_MACOS_VERSION "gstreamer_macos_version") qgc_config_get_value(QGC_CONFIG_GSTREAMER_WIN_VERSION "gstreamer_windows_version") qgc_config_get_value(QGC_CONFIG_NDK_VERSION "ndk_version") @@ -35,4 +37,21 @@ qgc_config_get_value(QGC_CONFIG_CMAKE_MINIMUM "cmake_minimum_version") qgc_config_get_value(QGC_CONFIG_MACOS_DEPLOYMENT_TARGET "macos_deployment_target") qgc_config_get_value(QGC_CONFIG_IOS_DEPLOYMENT_TARGET "ios_deployment_target") +# Extract patch versions from platform strings (e.g., "1.22.12" -> QGC_GSTREAMER_PATCH_1_22=12) +foreach(_gst_ver_var QGC_CONFIG_GSTREAMER_ANDROID_VERSION QGC_CONFIG_GSTREAMER_IOS_VERSION QGC_CONFIG_GSTREAMER_MACOS_VERSION QGC_CONFIG_GSTREAMER_WIN_VERSION) + if(DEFINED ${_gst_ver_var}) + string(REPLACE "." ";" _ver_parts "${${_gst_ver_var}}") + list(LENGTH _ver_parts _ver_len) + if(_ver_len EQUAL 3) + list(GET _ver_parts 0 _major) + list(GET _ver_parts 1 _minor) + list(GET _ver_parts 2 _patch) + set(_patch_var "QGC_GSTREAMER_PATCH_${_major}_${_minor}") + if(NOT DEFINED ${_patch_var}) + set(${_patch_var} "${_patch}" CACHE STRING "GStreamer ${_major}.${_minor} patch version (from ${_gst_ver_var})") + endif() + endif() + endif() +endforeach() + message(STATUS "BuildConfig: Qt ${QGC_CONFIG_QT_VERSION}, GStreamer ${QGC_CONFIG_GSTREAMER_VERSION}, NDK ${QGC_CONFIG_NDK_VERSION}") diff --git a/cmake/CustomOptions.cmake b/cmake/CustomOptions.cmake index 4207c7b4dfb4..481e989d4917 100644 --- a/cmake/CustomOptions.cmake +++ b/cmake/CustomOptions.cmake @@ -75,7 +75,6 @@ option(QGC_NO_SERIAL_LINK "Disable serial port communication" OFF) option(QGC_ENABLE_UVC "Enable UVC (USB Video Class) device support" ON) option(QGC_ENABLE_GST_VIDEOSTREAMING "Enable GStreamer video backend" ON) -cmake_dependent_option(QGC_CUSTOM_GST_PACKAGE "Use QGC-provided GStreamer packages" OFF "QGC_ENABLE_GST_VIDEOSTREAMING" OFF) option(QGC_ENABLE_QT_VIDEOSTREAMING "Enable QtMultimedia video backend" OFF) # ============================================================================ diff --git a/cmake/find-modules/FindGStreamer.cmake b/cmake/find-modules/FindGStreamer.cmake deleted file mode 100644 index 200da8569dd9..000000000000 --- a/cmake/find-modules/FindGStreamer.cmake +++ /dev/null @@ -1,672 +0,0 @@ -# ============================================================================ -# FindGStreamer.cmake -# CMake find module for GStreamer multimedia framework -# -# Handles multiple platforms: Windows, Linux, macOS, Android, iOS -# Supports both static and dynamic linking -# ============================================================================ - -# ---------------------------------------------------------------------------- -# Default Configuration -# ---------------------------------------------------------------------------- - -# Set default version based on platform (from build-config.json) -if(NOT DEFINED GStreamer_FIND_VERSION) - if(LINUX) - set(GStreamer_FIND_VERSION 1.20) - elseif(WIN32) - set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_WIN_VERSION}) - elseif(ANDROID) - set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_ANDROID_VERSION}) - elseif(MACOS OR IOS) - set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_MACOS_VERSION}) - else() - set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_VERSION}) - endif() -endif() - -# Determine GStreamer root directory from various environment variables -if(NOT DEFINED GStreamer_ROOT_DIR) - if(DEFINED GSTREAMER_ROOT) - set(GStreamer_ROOT_DIR ${GSTREAMER_ROOT}) - elseif(DEFINED GStreamer_ROOT) - set(GStreamer_ROOT_DIR ${GStreamer_ROOT}) - endif() - - if(DEFINED GStreamer_ROOT_DIR AND NOT EXISTS "${GStreamer_ROOT_DIR}") - message(STATUS "GStreamer: User-provided directory does not exist: ${GStreamer_ROOT_DIR}") - endif() -endif() - -# Static vs dynamic linking preference -if(NOT DEFINED GStreamer_USE_STATIC_LIBS) - if(ANDROID OR IOS) - set(GStreamer_USE_STATIC_LIBS ON) - else() - set(GStreamer_USE_STATIC_LIBS OFF) - endif() -endif() - -# Framework usage (macOS/iOS only) -if(NOT DEFINED GStreamer_USE_FRAMEWORK) - if(APPLE) - set(GStreamer_USE_FRAMEWORK ON) - else() - set(GStreamer_USE_FRAMEWORK OFF) - endif() -endif() - -# ============================================================================ -# Platform-Specific Configuration -# ============================================================================ - -macro(_gst_normalize_and_validate_root) - cmake_path(CONVERT "${GStreamer_ROOT_DIR}" TO_CMAKE_PATH_LIST GStreamer_ROOT_DIR NORMALIZE) - if(NOT EXISTS "${GStreamer_ROOT_DIR}") - message(FATAL_ERROR "GStreamer: SDK not found at '${GStreamer_ROOT_DIR}' — " - "check installation or set GStreamer_ROOT_DIR") - endif() -endmacro() - -set(PKG_CONFIG_ARGN) - -# ---------------------------------------------------------------------------- -# Windows Platform -# ---------------------------------------------------------------------------- -if(WIN32) - if(NOT DEFINED GStreamer_ROOT_DIR) - if(DEFINED ENV{GSTREAMER_1_0_ROOT_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_X86_64}") - set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_X86_64}") - elseif(MSVC AND DEFINED ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64}") - set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64}") - elseif(MINGW AND DEFINED ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64}") - set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64}") - elseif(EXISTS "C:/Program Files/gstreamer/1.0/msvc_x86_64") - set(GStreamer_ROOT_DIR "C:/Program Files/gstreamer/1.0/msvc_x86_64") - elseif(EXISTS "C:/gstreamer/1.0/msvc_x86_64") - set(GStreamer_ROOT_DIR "C:/gstreamer/1.0/msvc_x86_64") - endif() - endif() - - _gst_normalize_and_validate_root() - - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") - set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") - set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") - - set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/bin/pkg-config.exe") - # CACHE FORCE ensures find_package(PkgConfig) sees the bundled exe even when - # no system pkg-config is on PATH (common in CI containers). - set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) - set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig;${GSTREAMER_PLUGIN_PATH}/pkgconfig;$ENV{PKG_CONFIG_PATH}") - list(APPEND PKG_CONFIG_ARGN - --dont-define-prefix - --define-variable=prefix=${GStreamer_ROOT_DIR} - --define-variable=libdir=${GSTREAMER_LIB_PATH} - --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} - ) - -# ---------------------------------------------------------------------------- -# Linux Platform -# ---------------------------------------------------------------------------- -elseif(LINUX) - if(NOT DEFINED GStreamer_ROOT_DIR) - if(EXISTS "/usr") - set(GStreamer_ROOT_DIR "/usr") - endif() - endif() - - _gst_normalize_and_validate_root() - - if((EXISTS "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu") AND (EXISTS "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu/gstreamer-1.0")) - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu") - elseif((EXISTS "${GStreamer_ROOT_DIR}/lib64") AND (EXISTS "${GStreamer_ROOT_DIR}/lib64/gstreamer-1.0")) - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib64") - elseif((EXISTS "${GStreamer_ROOT_DIR}/lib") AND (EXISTS "${GStreamer_ROOT_DIR}/lib/gstreamer-1.0")) - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") - else() - message(FATAL_ERROR "Could not locate GStreamer libraries - check installation or set environment/cmake variables") - endif() - - set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") - set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") - - set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig:$ENV{PKG_CONFIG_PATH}") - -# ---------------------------------------------------------------------------- -# Android Platform -# ---------------------------------------------------------------------------- -elseif(ANDROID) - set(_gst_freedesktop_url "https://gstreamer.freedesktop.org/data/pkg/android/${GStreamer_FIND_VERSION}/gstreamer-1.0-android-universal-${GStreamer_FIND_VERSION}.tar.xz") - set(_gst_s3_url "https://qgroundcontrol.s3.us-west-2.amazonaws.com/dependencies/gstreamer/android/qgc-android-gstreamer-${GStreamer_FIND_VERSION}.tar.xz") - - set(_gst_android_hash "") - - if(QGC_CUSTOM_GST_PACKAGE) - set(_gst_android_urls "${_gst_s3_url}") - set(_gst_source_variant "custom") - elseif(CMAKE_ANDROID_ARCH_ABI MATCHES "^(x86|x86_64)$") - # S3 custom package only contains ARM architectures; use freedesktop universal tarball for x86 - set(_gst_android_urls "${_gst_freedesktop_url}") - set(_gst_source_variant "universal") - # Fetch checksum only for freedesktop-only downloads where it can be verified - set(_gst_checksum_url "${_gst_freedesktop_url}.sha256sum") - if(DEFINED ENV{CPM_SOURCE_CACHE}) - set(_gst_checksum_dir "$ENV{CPM_SOURCE_CACHE}") - else() - set(_gst_checksum_dir "${CMAKE_BINARY_DIR}/_deps") - endif() - set(_gst_checksum_file "${_gst_checksum_dir}/gstreamer-android-${GStreamer_FIND_VERSION}.sha256sum") - if(NOT EXISTS "${_gst_checksum_file}") - file(MAKE_DIRECTORY "${_gst_checksum_dir}") - file(DOWNLOAD "${_gst_checksum_url}" "${_gst_checksum_file}" - STATUS _checksum_status TIMEOUT 30 TLS_VERIFY ON) - list(GET _checksum_status 0 _checksum_code) - if(NOT _checksum_code EQUAL 0) - file(REMOVE "${_gst_checksum_file}") - endif() - endif() - if(EXISTS "${_gst_checksum_file}") - file(READ "${_gst_checksum_file}" _checksum_content) - string(STRIP "${_checksum_content}" _checksum_content) - string(REGEX MATCH "([0-9a-fA-F]{64})" _checksum_match "${_checksum_content}") - if(_checksum_match) - set(_gst_android_hash "SHA256=${CMAKE_MATCH_1}") - endif() - endif() - else() - set(_gst_android_urls "${_gst_freedesktop_url}" "${_gst_s3_url}") - set(_gst_source_variant "universal") - endif() - - # Download with retry before passing to CPM (avoids FetchContent timeout failures) - if(DEFINED ENV{CPM_SOURCE_CACHE}) - set(_gst_download_dir "$ENV{CPM_SOURCE_CACHE}/gstreamer-android") - else() - set(_gst_download_dir "${CMAKE_BINARY_DIR}/_deps/gstreamer-android") - endif() - set(_gst_archive_name "gstreamer-android-${_gst_source_variant}-${GStreamer_FIND_VERSION}.tar.xz") - set(_gst_archive_path "${_gst_download_dir}/${_gst_archive_name}") - - # Extract raw hash from "SHA256=" format - set(_gst_expected_hash "") - if(_gst_android_hash) - string(REGEX MATCH "=([0-9a-fA-F]+)$" _gst_hash_match "${_gst_android_hash}") - if(_gst_hash_match) - set(_gst_expected_hash "${CMAKE_MATCH_1}") - endif() - endif() - - # Verify cached archive integrity - set(_gst_need_download TRUE) - if(EXISTS "${_gst_archive_path}") - if(_gst_expected_hash) - file(SHA256 "${_gst_archive_path}" _gst_cached_hash) - if(_gst_cached_hash STREQUAL "${_gst_expected_hash}") - message(STATUS "GStreamer: Using cached ${_gst_archive_path} (SHA256 verified)") - set(_gst_need_download FALSE) - else() - message(STATUS "GStreamer: Cached archive failed SHA256 check, re-downloading") - file(REMOVE "${_gst_archive_path}") - endif() - else() - message(STATUS "GStreamer: Using cached ${_gst_archive_path}") - set(_gst_need_download FALSE) - endif() - endif() - - if(_gst_need_download) - file(MAKE_DIRECTORY "${_gst_download_dir}") - set(_gst_downloaded FALSE) - foreach(_gst_url IN LISTS _gst_android_urls) - message(STATUS "GStreamer: Downloading from ${_gst_url}") - file(DOWNLOAD "${_gst_url}" "${_gst_archive_path}.tmp" - STATUS _gst_dl_status - SHOW_PROGRESS - TIMEOUT 600 - INACTIVITY_TIMEOUT 60 - TLS_VERIFY ON - ) - list(GET _gst_dl_status 0 _gst_dl_code) - if(_gst_dl_code EQUAL 0) - if(_gst_expected_hash) - file(SHA256 "${_gst_archive_path}.tmp" _gst_actual_hash) - if(NOT _gst_actual_hash STREQUAL "${_gst_expected_hash}") - message(WARNING "GStreamer: SHA256 mismatch from ${_gst_url}, trying next URL") - file(REMOVE "${_gst_archive_path}.tmp") - continue() - endif() - endif() - file(RENAME "${_gst_archive_path}.tmp" "${_gst_archive_path}") - set(_gst_downloaded TRUE) - break() - endif() - list(GET _gst_dl_status 1 _gst_dl_error) - message(WARNING "GStreamer: Download failed from ${_gst_url}: ${_gst_dl_error}") - file(REMOVE "${_gst_archive_path}.tmp") - endforeach() - if(NOT _gst_downloaded) - message(FATAL_ERROR "GStreamer: All download URLs failed for ${_gst_archive_name}.\n" - "Tried: ${_gst_android_urls}\n" - "Set GStreamer_ROOT_DIR to use a local installation.") - endif() - endif() - - CPMAddPackage( - NAME gstreamer - VERSION ${GStreamer_FIND_VERSION} - URL "file://${_gst_archive_path}" - DOWNLOAD_EXTRACT_TIMESTAMP ON - ) - - if(NOT DEFINED GStreamer_ROOT_DIR) - if(CMAKE_ANDROID_ARCH_ABI STREQUAL "armeabi-v7a") - set(_gst_arch "armv7") - elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") - set(_gst_arch "arm64") - elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86") - set(_gst_arch "x86") - elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64") - set(_gst_arch "x86_64") - else() - message(FATAL_ERROR "GStreamer: Unsupported Android ABI: ${CMAKE_ANDROID_ARCH_ABI}") - endif() - - set(GStreamer_ROOT_DIR "${gstreamer_SOURCE_DIR}/${_gst_arch}") - endif() - - cmake_path(CONVERT "${GStreamer_ROOT_DIR}" TO_CMAKE_PATH_LIST GStreamer_ROOT_DIR NORMALIZE) - if(NOT EXISTS "${GStreamer_ROOT_DIR}") - file(GLOB _gst_extracted_dirs LIST_DIRECTORIES true "${gstreamer_SOURCE_DIR}/*") - message(FATAL_ERROR - "Could not locate GStreamer for ${CMAKE_ANDROID_ARCH_ABI}\n" - " Expected: ${GStreamer_ROOT_DIR}\n" - " Source dir: ${gstreamer_SOURCE_DIR}\n" - " Contents: ${_gst_extracted_dirs}\n" - " Check that the GStreamer tarball contains an '${_gst_arch}' directory." - ) - endif() - - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") - set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") - set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") - - # Clear host PKG_CONFIG_PATH to prevent host .pc files contaminating cross-builds. - # Use PKG_CONFIG_LIBDIR instead to restrict search to GStreamer SDK paths only. - set(ENV{PKG_CONFIG_PATH} "") - if(CMAKE_HOST_WIN32) - set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/share/gst-android/ndk-build/tools/windows/pkg-config.exe") - set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) - set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig;${GSTREAMER_PLUGIN_PATH}/pkgconfig") - list(APPEND PKG_CONFIG_ARGN --dont-define-prefix) - elseif(CMAKE_HOST_UNIX) - if(CMAKE_HOST_APPLE) - # Try to find pkg-config in common Homebrew locations and in PATH - find_program(PKG_CONFIG_EXECUTABLE - NAMES pkg-config - PATHS /opt/homebrew/bin /usr/local/bin - NO_DEFAULT_PATH - ) - if(NOT PKG_CONFIG_EXECUTABLE) - # Fallback to PATH search if not found in Homebrew locations - find_program(PKG_CONFIG_EXECUTABLE pkg-config) - endif() - if(NOT PKG_CONFIG_EXECUTABLE) - message(FATAL_ERROR "Could not find pkg-config. Please install pkg-config using tools/setup/install_dependencies.py --platform macos.") - endif() - endif() - set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") - endif() - list(APPEND PKG_CONFIG_ARGN - --define-variable=prefix=${GStreamer_ROOT_DIR} - --define-variable=libdir=${GSTREAMER_LIB_PATH} - --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} - ) - -# ---------------------------------------------------------------------------- -# macOS Platform -# ---------------------------------------------------------------------------- -elseif(MACOS) - if(NOT DEFINED GStreamer_ROOT_DIR) - if(EXISTS "/Library/Frameworks/GStreamer.framework") - set(GStreamer_ROOT_DIR "/Library/Frameworks/GStreamer.framework/Versions/1.0") - elseif(EXISTS "/opt/homebrew/opt/gstreamer") - set(GStreamer_ROOT_DIR "/opt/homebrew/opt/gstreamer") - set(GStreamer_USE_FRAMEWORK OFF) - elseif(EXISTS "/usr/local/opt/gstreamer") - set(GStreamer_ROOT_DIR "/usr/local/opt/gstreamer") - set(GStreamer_USE_FRAMEWORK OFF) - endif() - endif() - - _gst_normalize_and_validate_root() - - if(GStreamer_USE_FRAMEWORK AND NOT DEFINED GSTREAMER_FRAMEWORK_PATH) - set(GSTREAMER_FRAMEWORK_PATH "${GStreamer_ROOT_DIR}/../..") - cmake_path(NORMAL_PATH GSTREAMER_FRAMEWORK_PATH) - endif() - - set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") - set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") - - if(GStreamer_USE_FRAMEWORK) - set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/bin/pkg-config") - set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) - set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig:$ENV{PKG_CONFIG_PATH}") - else() - find_program(PKG_CONFIG_EXECUTABLE NAMES pkg-config pkgconf - PATHS /opt/homebrew/bin /usr/local/bin - NO_DEFAULT_PATH - ) - if(NOT PKG_CONFIG_EXECUTABLE) - find_program(PKG_CONFIG_EXECUTABLE NAMES pkg-config pkgconf) - endif() - set(ENV{PKG_CONFIG_PATH} "") - set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") - endif() - list(APPEND PKG_CONFIG_ARGN - --define-variable=prefix=${GStreamer_ROOT_DIR} - --define-variable=libdir=${GSTREAMER_LIB_PATH} - --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} - ) - -# ---------------------------------------------------------------------------- -# iOS Platform (Currently Unsupported) -# ---------------------------------------------------------------------------- -elseif(IOS) - message(FATAL_ERROR "GStreamer for iOS is currently unsupported") - - CPMAddPackage( - NAME gstreamer - VERSION ${GStreamer_FIND_VERSION} - URL "https://gstreamer.freedesktop.org/data/pkg/ios/${GStreamer_FIND_VERSION}/gstreamer-1.0-devel-${GStreamer_FIND_VERSION}-ios-universal.pkg" - ) - - set(GST_PKG_FILE "${gstreamer_SOURCE_DIR}/gstreamer.pkg") - set(GST_EXPAND_DIR "${gstreamer_SOURCE_DIR}/gstreamer-pkg-expanded") - set(GST_PAYLOAD_DIR "${GST_EXPAND_DIR}/Payload") - - file(MAKE_DIRECTORY "${GST_EXPAND_DIR}") - execute_process( - COMMAND pkgutil --expand-full "${GST_PKG_FILE}" "${GST_EXPAND_DIR}" - RESULT_VARIABLE _pkgutil_rc - ) - if(NOT _pkgutil_rc EQUAL 0) - message(FATAL_ERROR "pkgutil failed to expand GStreamer .pkg") - endif() - - execute_process( - COMMAND xar -xf "${GST_EXPAND_DIR}/gstreamer-1.0-devel-${GStreamer_FIND_VERSION}-ios-universal.pkg/Payload" - --directory "${GST_PAYLOAD_DIR}" - RESULT_VARIABLE _xar_rc - ) - if(NOT _xar_rc EQUAL 0) - message(FATAL_ERROR "xar failed to extract GStreamer Payload") - endif() - - # set(GSTREAMER_FRAMEWORK_PATH "/Library/Developer/GStreamer/iPhone.sdk" CACHE PATH "Path of GStreamer.Framework") - set(GSTREAMER_FRAMEWORK_PATH "${GST_PAYLOAD_DIR}/usr/local/Frameworks/GStreamer.framework") - - set(GStreamer_ROOT_DIR "${GSTREAMER_FRAMEWORK_PATH}/Versions/1.0") - - if(NOT EXISTS "${GStreamer_ROOT_DIR}") - message(FATAL_ERROR "Could not locate GStreamer - check installation or set environment/cmake variables") - endif() - - set(GSTREAMER_INCLUDE_PATH "${GSTREAMER_FRAMEWORK_PATH}/Headers") -endif() - -# ---------------------------------------------------------------------------- -# Validation -# ---------------------------------------------------------------------------- -if(NOT EXISTS "${GStreamer_ROOT_DIR}" OR NOT EXISTS "${GSTREAMER_LIB_PATH}" OR NOT EXISTS "${GSTREAMER_PLUGIN_PATH}" OR NOT EXISTS "${GSTREAMER_INCLUDE_PATH}") - message(FATAL_ERROR "GStreamer: Could not locate required directories - check installation or set GStreamer_ROOT_DIR") -endif() - -if(GStreamer_USE_FRAMEWORK AND NOT EXISTS "${GSTREAMER_FRAMEWORK_PATH}") - message(FATAL_ERROR "GStreamer: Could not locate framework at ${GSTREAMER_FRAMEWORK_PATH}") -endif() - -# ============================================================================ -# Plugin & Dependency Configuration -# ============================================================================ - -if(GStreamer_USE_STATIC_LIBS) - set(GSTREAMER_EXTRA_DEPS - gstreamer-base-1.0 - gstreamer-video-1.0 - gstreamer-gl-1.0 - gstreamer-gl-prototypes-1.0 - gstreamer-rtsp-1.0 - # gstreamer-gl-egl-1.0 - # gstreamer-gl-wayland-1.0 - # gstreamer-gl-x11-1.0 - ) - - set(GSTREAMER_PLUGINS - coreelements - dav1d - isomp4 - libav - matroska - mpegtsdemux - opengl - openh264 - playback - rtp - rtpmanager - rtsp - sdpelem - tcp - typefindfunctions - udp - videoparsersbad - vpx - ) - if(ANDROID) - list(APPEND GSTREAMER_PLUGINS androidmedia) # vulkan - elseif(APPLE) - list(APPEND GSTREAMER_PLUGINS applemedia vulkan) - elseif(WIN32) - list(APPEND GSTREAMER_PLUGINS d3d d3d11 d3d12 dxva nvcodec) - elseif(LINUX) - list(APPEND GSTREAMER_PLUGINS nvcodec qsv va vulkan) # qml6 - GStreamer provided qml6 is xcb only - endif() -endif() - -if(ANDROID) - set(GStreamer_Mobile_MODULE_NAME gstreamer_android) - set(G_IO_MODULES openssl) - set(G_IO_MODULES_PATH "${GStreamer_ROOT_DIR}/lib/gio/modules") - - set(GStreamer_NDK_BUILD_PATH "${GStreamer_ROOT_DIR}/share/gst-android/ndk-build/") - set(GSTREAMER_ANDROID_MODULE_NAME gstreamer_android) - set(GSTREAMER_JAVA_SRC_DIR "${CMAKE_BINARY_DIR}/android-build-${CMAKE_PROJECT_NAME}/src") - set(GSTREAMER_ASSETS_DIR "${CMAKE_BINARY_DIR}/android-build-${CMAKE_PROJECT_NAME}/assets") - - configure_file( - "${GStreamer_NDK_BUILD_PATH}/gstreamer_android-1.0.c.in" - "${GStreamer_Mobile_MODULE_NAME}.c" - ) -endif() - -# ============================================================================ -# Package Discovery -# ============================================================================ - -set(GStreamer_ROOT_DIR "${GStreamer_ROOT_DIR}" CACHE PATH "GStreamer SDK root directory" FORCE) - -if(GStreamer_USE_FRAMEWORK) - list(APPEND CMAKE_FRAMEWORK_PATH "${GSTREAMER_FRAMEWORK_PATH}") -endif() - -if(GStreamer_USE_STATIC_LIBS) - list(APPEND PKG_CONFIG_ARGN "--static") -endif() - -find_package(PkgConfig REQUIRED QUIET) - -list(PREPEND CMAKE_PREFIX_PATH ${GStreamer_ROOT_DIR}) -if(LINUX) - pkg_check_modules(PC_GSTREAMER REQUIRED gstreamer-1.0>=${GStreamer_FIND_VERSION}) -else() - pkg_check_modules(PC_GSTREAMER REQUIRED gstreamer-1.0=${GStreamer_FIND_VERSION}) -endif() -set(GStreamer_VERSION "${PC_GSTREAMER_VERSION}") - -# ============================================================================ -# Component Discovery -# ============================================================================ - -# Helper function to find individual GStreamer components -function(find_gstreamer_component component pkgconfig_name) - set(target GStreamer::${component}) - - if(NOT TARGET ${target}) - string(TOUPPER ${component} upper) - pkg_check_modules(PC_GSTREAMER_${upper} IMPORTED_TARGET ${pkgconfig_name}) - if(TARGET PkgConfig::PC_GSTREAMER_${upper}) - qt_add_library(GStreamer::${component} INTERFACE IMPORTED) - target_link_libraries(GStreamer::${component} INTERFACE PkgConfig::PC_GSTREAMER_${upper}) - if("PC_GSTREAMER_${upper}" MATCHES "PC_GSTREAMER_GL") - get_target_property(_qt_incs PkgConfig::PC_GSTREAMER_GL INTERFACE_INCLUDE_DIRECTORIES) - set(__qt_fixed_incs) - foreach(path IN LISTS _qt_incs) - if(IS_DIRECTORY "${path}") - list(APPEND __qt_fixed_incs "${path}") - endif() - endforeach() - set_property(TARGET PkgConfig::PC_GSTREAMER_GL PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${__qt_fixed_incs}") - endif() - endif() - endif() - - if(TARGET ${target}) - set(GStreamer_${component}_FOUND TRUE PARENT_SCOPE) - endif() -endfunction() - -# ---------------------------------------------------------------------------- -# Find Core Components (Always Required) -# ---------------------------------------------------------------------------- -find_gstreamer_component(Core gstreamer-1.0) -find_gstreamer_component(Base gstreamer-base-1.0) -find_gstreamer_component(Video gstreamer-video-1.0) -find_gstreamer_component(Gl gstreamer-gl-1.0) -find_gstreamer_component(GlPrototypes gstreamer-gl-prototypes-1.0) -find_gstreamer_component(Rtsp gstreamer-rtsp-1.0) - -# ---------------------------------------------------------------------------- -# Find Optional Components (Based on FIND_COMPONENTS) -# ---------------------------------------------------------------------------- -if(GlEgl IN_LIST GStreamer_FIND_COMPONENTS) - find_gstreamer_component(GlEgl gstreamer-gl-egl-1.0) -endif() - -if(GlWayland IN_LIST GStreamer_FIND_COMPONENTS) - find_gstreamer_component(GlWayland gstreamer-gl-wayland-1.0) -endif() - -if(GlX11 IN_LIST GStreamer_FIND_COMPONENTS) - find_gstreamer_component(GlX11 gstreamer-gl-x11-1.0) -endif() - -# ============================================================================ -# Package Finalization -# ============================================================================ - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(GStreamer - VERSION_VAR GStreamer_VERSION - HANDLE_COMPONENTS -) - -# ---------------------------------------------------------------------------- -# Create Main GStreamer Target -# ---------------------------------------------------------------------------- -if(GStreamer_FOUND AND NOT TARGET GStreamer::GStreamer) - qt_add_library(GStreamer::GStreamer INTERFACE IMPORTED) - - if(GStreamer_USE_STATIC_LIBS) - target_compile_definitions(GStreamer::GStreamer INTERFACE QGC_GST_STATIC_BUILD) - endif() - - if(APPLE AND GStreamer_USE_FRAMEWORK) - set(CMAKE_FIND_FRAMEWORK ONLY) - find_library(GSTREAMER_FRAMEWORK GStreamer - PATHS - "${GSTREAMER_FRAMEWORK_PATH}" - "/Library/Frameworks" - "/usr/local/opt/gstreamer" - "/opt/homebrew/opt/gstreamer" - ) - unset(CMAKE_FIND_FRAMEWORK) - if(GSTREAMER_FRAMEWORK) - target_link_libraries(GStreamer::GStreamer INTERFACE ${GSTREAMER_FRAMEWORK}) - target_include_directories(GStreamer::GStreamer INTERFACE "${GSTREAMER_FRAMEWORK}/Headers") - if(MACOS) - target_compile_definitions(GStreamer::GStreamer INTERFACE QGC_GST_MACOS_FRAMEWORK) - endif() - return() - else() - message(FATAL_ERROR "GStreamer: Could not locate GStreamer.framework") - endif() - - # Android-specific link options - elseif(ANDROID) - target_link_options(GStreamer::GStreamer INTERFACE "-Wl,-Bsymbolic") - if(CMAKE_SIZEOF_VOID_P EQUAL 4) - target_link_options(GStreamer::GStreamer INTERFACE "-Wl,-z,notext") - endif() - endif() - - target_link_directories(GStreamer::GStreamer INTERFACE ${GSTREAMER_LIB_PATH}) - - target_link_libraries(GStreamer::GStreamer - INTERFACE - GStreamer::Core - GStreamer::Base - GStreamer::Video - GStreamer::Gl - GStreamer::GlPrototypes - GStreamer::Rtsp - ) - - foreach(component IN LISTS GStreamer_FIND_COMPONENTS) - if(GStreamer_${component}_FOUND) - target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::${component}) - endif() - endforeach() - -# ---------------------------------------------------------------------------- -# Static Plugin Linking (Static Builds Only) -# ---------------------------------------------------------------------------- - if(GStreamer_USE_STATIC_LIBS) - qt_add_library(GStreamer::Plugins INTERFACE IMPORTED) - target_link_directories(GStreamer::Plugins INTERFACE ${GSTREAMER_PLUGIN_PATH}) - - foreach(plugin IN LISTS GSTREAMER_PLUGINS) - pkg_check_modules(GST_PLUGIN_${plugin} QUIET IMPORTED_TARGET gst${plugin}) - if(GST_PLUGIN_${plugin}_FOUND) - target_link_libraries(GStreamer::Plugins INTERFACE PkgConfig::GST_PLUGIN_${plugin}) - else() - find_library(GST_PLUGIN_${plugin}_LIBRARY - NAMES gst${plugin} - PATHS - ${GSTREAMER_LIB_PATH} - ${GSTREAMER_PLUGIN_PATH} - ) - if(GST_PLUGIN_${plugin}_LIBRARY) - target_link_libraries(GStreamer::Plugins INTERFACE ${GST_PLUGIN_${plugin}_LIBRARY}) - set(GST_PLUGIN_${plugin}_FOUND TRUE) - endif() - endif() - if(GST_PLUGIN_${plugin}_FOUND) - target_compile_definitions(GStreamer::Plugins INTERFACE GST_PLUGIN_${plugin}_FOUND) - endif() - endforeach() - - target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::Plugins) - endif() -endif() diff --git a/cmake/find-modules/FindQGCGStreamer.cmake b/cmake/find-modules/FindQGCGStreamer.cmake new file mode 100644 index 000000000000..0d470258302b --- /dev/null +++ b/cmake/find-modules/FindQGCGStreamer.cmake @@ -0,0 +1,592 @@ +if(NOT DEFINED GStreamer_FIND_VERSION) + if(LINUX) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_MIN_VERSION}) + elseif(WIN32) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_WIN_VERSION}) + elseif(ANDROID) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_ANDROID_VERSION}) + elseif(IOS) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_IOS_VERSION}) + elseif(MACOS) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_MACOS_VERSION}) + else() + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_VERSION}) + endif() +endif() + +if(NOT GStreamer_FIND_VERSION) + message(FATAL_ERROR "GStreamer version not configured. Ensure BuildConfig.cmake has been included " + "and .github/build-config.json contains the appropriate gstreamer_*_version entry.") +endif() + +if(NOT DEFINED GStreamer_ROOT_DIR) + if(DEFINED GSTREAMER_ROOT) + set(GStreamer_ROOT_DIR ${GSTREAMER_ROOT}) + elseif(DEFINED GStreamer_ROOT) + set(GStreamer_ROOT_DIR ${GStreamer_ROOT}) + endif() + + if(DEFINED GStreamer_ROOT_DIR AND NOT EXISTS "${GStreamer_ROOT_DIR}") + message(FATAL_ERROR "GStreamer: User-provided directory does not exist: ${GStreamer_ROOT_DIR}\n" + "Correct the path or unset GStreamer_ROOT_DIR.") + endif() +endif() + +if(NOT DEFINED GStreamer_USE_STATIC_LIBS) + if(ANDROID OR IOS) + set(GStreamer_USE_STATIC_LIBS ON) + else() + set(GStreamer_USE_STATIC_LIBS OFF) + endif() +endif() + +if(NOT DEFINED GStreamer_USE_FRAMEWORK) + if(APPLE) + set(GStreamer_USE_FRAMEWORK ON) + else() + set(GStreamer_USE_FRAMEWORK OFF) + endif() +endif() + +include(GStreamerHelpers) + +# PKG_CONFIG_ARGN is consumed by CMake's FindPkgConfig module (3.22+). +# Extra flags appended here are forwarded to every pkg_check_modules() call. +set(PKG_CONFIG_ARGN) + +function(_qgc_find_apple_pkg_config OUT_VAR) + find_program(_qgc_pkg_config + NAMES pkg-config pkgconf + PATHS /opt/homebrew/bin /usr/local/bin + NO_DEFAULT_PATH + ) + if(NOT _qgc_pkg_config) + find_program(_qgc_pkg_config NAMES pkg-config pkgconf) + endif() + if(NOT _qgc_pkg_config) + message(FATAL_ERROR + "Could not find pkg-config.\n" + "Install dependencies with: python3 tools/setup/install_dependencies.py --platform macos\n" + "or install pkg-config manually (for example: brew install pkg-config).") + endif() + set(${OUT_VAR} "${_qgc_pkg_config}" CACHE FILEPATH "pkg-config executable" FORCE) + unset(_qgc_pkg_config CACHE) +endfunction() + +function(_qgc_validate_expanded_pkg EXPANDED_DIR LABEL) + file(GLOB _payloads "${EXPANDED_DIR}/*.pkg/Payload") + if(NOT _payloads) + file(REMOVE_RECURSE "${EXPANDED_DIR}") + message(FATAL_ERROR + "pkgutil expanded GStreamer ${LABEL} package but no payloads were found in ${EXPANDED_DIR}") + endif() +endfunction() + +if(WIN32) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") + set(_gst_win_arch "arm64") + else() + set(_gst_win_arch "x86_64") + endif() + + if(NOT DEFINED GStreamer_ROOT_DIR) + if(_gst_win_arch STREQUAL "arm64") + if(DEFINED ENV{GSTREAMER_1_0_ROOT_ARM64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_ARM64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_ARM64}") + elseif(MSVC AND DEFINED ENV{GSTREAMER_1_0_ROOT_MSVC_ARM64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MSVC_ARM64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MSVC_ARM64}") + elseif(EXISTS "C:/Program Files/gstreamer/1.0/msvc_arm64") + set(GStreamer_ROOT_DIR "C:/Program Files/gstreamer/1.0/msvc_arm64") + elseif(EXISTS "C:/gstreamer/1.0/msvc_arm64") + set(GStreamer_ROOT_DIR "C:/gstreamer/1.0/msvc_arm64") + endif() + else() + if(DEFINED ENV{GSTREAMER_1_0_ROOT_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_X86_64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_X86_64}") + elseif(MSVC AND DEFINED ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64}") + elseif(MINGW AND DEFINED ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64}") + elseif(EXISTS "C:/Program Files/gstreamer/1.0/msvc_x86_64") + set(GStreamer_ROOT_DIR "C:/Program Files/gstreamer/1.0/msvc_x86_64") + elseif(EXISTS "C:/gstreamer/1.0/msvc_x86_64") + set(GStreamer_ROOT_DIR "C:/gstreamer/1.0/msvc_x86_64") + endif() + endif() + endif() + + _gst_normalize_and_validate_root() + + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") + + set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/bin/pkg-config.exe") + set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) + set(ENV{PKG_CONFIG_PATH} "") + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig;${GSTREAMER_PLUGIN_PATH}/pkgconfig") + list(APPEND PKG_CONFIG_ARGN + --dont-define-prefix + --define-variable=prefix=${GStreamer_ROOT_DIR} + --define-variable=libdir=${GSTREAMER_LIB_PATH} + --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} + ) + +elseif(LINUX) + if(NOT DEFINED GStreamer_ROOT_DIR) + set(GStreamer_ROOT_DIR "/usr") + endif() + + _gst_normalize_and_validate_root() + + if((EXISTS "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu") AND (EXISTS "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu/gstreamer-1.0")) + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu") + elseif((EXISTS "${GStreamer_ROOT_DIR}/lib64") AND (EXISTS "${GStreamer_ROOT_DIR}/lib64/gstreamer-1.0")) + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib64") + elseif((EXISTS "${GStreamer_ROOT_DIR}/lib") AND (EXISTS "${GStreamer_ROOT_DIR}/lib/gstreamer-1.0")) + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + else() + message(FATAL_ERROR "Could not locate GStreamer libraries - check installation or set environment/cmake variables") + endif() + + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") + + set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig:$ENV{PKG_CONFIG_PATH}") + +elseif(ANDROID) + gstreamer_get_package_url(android ${GStreamer_FIND_VERSION} _gst_android_url) + gstreamer_get_s3_mirror_url(android ${GStreamer_FIND_VERSION} _gst_android_s3_url) + gstreamer_fetch_checksum(android ${GStreamer_FIND_VERSION} _gst_android_hash) + + if(CPM_SOURCE_CACHE) + set(_gst_android_cache "${CPM_SOURCE_CACHE}/gstreamer-android") + else() + set(_gst_android_cache "${CMAKE_BINARY_DIR}/_deps/gstreamer-android") + endif() + gstreamer_resilient_download( + URLS "${_gst_android_url}" "${_gst_android_s3_url}" + FILENAME "gstreamer-android-${GStreamer_FIND_VERSION}.tar.xz" + DESTINATION_DIR "${_gst_android_cache}" + RESULT_VAR _gst_android_archive + EXPECTED_HASH "${_gst_android_hash}" + ) + + CPMAddPackage( + NAME gstreamer + VERSION ${GStreamer_FIND_VERSION} + URL "file://${_gst_android_archive}" + ) + + if(NOT DEFINED GStreamer_ROOT_DIR) + if(CMAKE_ANDROID_ARCH_ABI STREQUAL "armeabi-v7a") + set(GStreamer_ABI_DIR "armv7") + elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") + set(GStreamer_ABI_DIR "arm64") + elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86") + set(GStreamer_ABI_DIR "x86") + elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64") + set(GStreamer_ABI_DIR "x86_64") + else() + message(FATAL_ERROR "Unsupported Android ABI: ${CMAKE_ANDROID_ARCH_ABI}") + endif() + set(GStreamer_ROOT_DIR "${gstreamer_SOURCE_DIR}/${GStreamer_ABI_DIR}") + set(GStreamer_AUTO_DOWNLOADED TRUE) + endif() + + _gst_normalize_and_validate_root() + + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") + + set(ENV{PKG_CONFIG_PATH} "") + if(CMAKE_HOST_WIN32) + set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/share/gst-android/ndk-build/tools/windows/pkg-config.exe") + set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig;${GSTREAMER_PLUGIN_PATH}/pkgconfig") + list(APPEND PKG_CONFIG_ARGN --dont-define-prefix) + elseif(CMAKE_HOST_UNIX) + if(CMAKE_HOST_APPLE) + _qgc_find_apple_pkg_config(PKG_CONFIG_EXECUTABLE) + endif() + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") + endif() + list(APPEND PKG_CONFIG_ARGN + --define-variable=prefix=${GStreamer_ROOT_DIR} + --define-variable=libdir=${GSTREAMER_LIB_PATH} + --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} + ) + +elseif(MACOS) + if(NOT DEFINED GStreamer_ROOT_DIR) + if(EXISTS "/Library/Frameworks/GStreamer.framework") + set(GStreamer_ROOT_DIR "/Library/Frameworks/GStreamer.framework/Versions/1.0") + elseif(EXISTS "/opt/homebrew/opt/gstreamer") + set(GStreamer_ROOT_DIR "/opt/homebrew/opt/gstreamer") + set(GStreamer_USE_FRAMEWORK OFF) + elseif(EXISTS "/usr/local/opt/gstreamer") + set(GStreamer_ROOT_DIR "/usr/local/opt/gstreamer") + set(GStreamer_USE_FRAMEWORK OFF) + endif() + endif() + + _gst_normalize_and_validate_root() + + if(GStreamer_USE_FRAMEWORK AND NOT DEFINED GSTREAMER_FRAMEWORK_PATH) + set(GSTREAMER_FRAMEWORK_PATH "${GStreamer_ROOT_DIR}/../..") + cmake_path(NORMAL_PATH GSTREAMER_FRAMEWORK_PATH) + endif() + + set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + + if(GStreamer_USE_FRAMEWORK) + set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/bin/pkg-config") + set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) + set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig:$ENV{PKG_CONFIG_PATH}") + else() + _qgc_find_apple_pkg_config(PKG_CONFIG_EXECUTABLE) + set(ENV{PKG_CONFIG_PATH} "") + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") + endif() + list(APPEND PKG_CONFIG_ARGN + --define-variable=prefix=${GStreamer_ROOT_DIR} + --define-variable=libdir=${GSTREAMER_LIB_PATH} + --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} + ) + +elseif(IOS) + if(NOT CMAKE_HOST_APPLE) + message(FATAL_ERROR "GStreamer for iOS can only be built on macOS") + endif() + + if(NOT DEFINED GStreamer_ROOT_DIR) + if(EXISTS "/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + set(GSTREAMER_FRAMEWORK_PATH "/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + set(GStreamer_ROOT_DIR "${GSTREAMER_FRAMEWORK_PATH}/Versions/1.0") + endif() + endif() + + if(NOT DEFINED GStreamer_ROOT_DIR OR NOT EXISTS "${GStreamer_ROOT_DIR}") + gstreamer_get_package_url(ios ${GStreamer_FIND_VERSION} _gst_ios_url) + gstreamer_get_s3_mirror_url(ios ${GStreamer_FIND_VERSION} _gst_ios_s3_url) + gstreamer_fetch_checksum(ios ${GStreamer_FIND_VERSION} _gst_ios_hash) + + set(_gst_ios_cache_dir "${CMAKE_BINARY_DIR}/_deps/gstreamer-ios-${GStreamer_FIND_VERSION}") + set(_gst_ios_expanded "${_gst_ios_cache_dir}/expanded") + + gstreamer_resilient_download( + URLS "${_gst_ios_url}" "${_gst_ios_s3_url}" + FILENAME "gstreamer-ios.pkg" + DESTINATION_DIR "${_gst_ios_cache_dir}" + RESULT_VAR _gst_ios_pkg + EXPECTED_HASH "${_gst_ios_hash}" + ) + + if(EXISTS "${_gst_ios_expanded}") + set(_gst_ios_complete FALSE) + file(GLOB _existing_pkg_dirs "${_gst_ios_expanded}/*.pkg") + foreach(_dir IN LISTS _existing_pkg_dirs) + if(EXISTS "${_dir}/Payload/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + set(_gst_ios_complete TRUE) + break() + endif() + endforeach() + if(NOT _gst_ios_complete) + message(STATUS "GStreamer: cached iOS expansion is incomplete; re-expanding") + file(REMOVE_RECURSE "${_gst_ios_expanded}") + endif() + endif() + + if(NOT EXISTS "${_gst_ios_expanded}") + message(STATUS "Expanding GStreamer iOS package...") + execute_process( + COMMAND pkgutil --expand-full "${_gst_ios_pkg}" "${_gst_ios_expanded}" + RESULT_VARIABLE _pkgutil_rc + ) + if(NOT _pkgutil_rc EQUAL 0) + file(REMOVE_RECURSE "${_gst_ios_expanded}") + message(FATAL_ERROR + "pkgutil failed to expand GStreamer iOS .pkg (exit code: ${_pkgutil_rc})") + endif() + _qgc_validate_expanded_pkg("${_gst_ios_expanded}" "iOS") + endif() + + file(GLOB _pkg_dirs "${_gst_ios_expanded}/*.pkg") + foreach(_pkg_dir IN LISTS _pkg_dirs) + if(EXISTS "${_pkg_dir}/Payload/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + set(GSTREAMER_FRAMEWORK_PATH "${_pkg_dir}/Payload/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + break() + endif() + endforeach() + + if(NOT GSTREAMER_FRAMEWORK_PATH) + message(FATAL_ERROR "Could not locate GStreamer.framework in downloaded iOS SDK") + endif() + + set(GStreamer_ROOT_DIR "${GSTREAMER_FRAMEWORK_PATH}/Versions/1.0") + set(GStreamer_AUTO_DOWNLOADED TRUE) + endif() + + _gst_normalize_and_validate_root() + + set(GStreamer_USE_FRAMEWORK ON) + if(NOT DEFINED GSTREAMER_FRAMEWORK_PATH) + set(GSTREAMER_FRAMEWORK_PATH "${GStreamer_ROOT_DIR}/../..") + cmake_path(NORMAL_PATH GSTREAMER_FRAMEWORK_PATH) + endif() + set(GSTREAMER_INCLUDE_PATH "${GSTREAMER_FRAMEWORK_PATH}/Headers") + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + + _qgc_find_apple_pkg_config(PKG_CONFIG_EXECUTABLE) + + set(ENV{PKG_CONFIG_PATH} "") + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") + list(APPEND PKG_CONFIG_ARGN + --define-variable=prefix=${GStreamer_ROOT_DIR} + --define-variable=libdir=${GSTREAMER_LIB_PATH} + --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} + ) +endif() + +if(NOT EXISTS "${GStreamer_ROOT_DIR}" OR NOT EXISTS "${GSTREAMER_LIB_PATH}" OR NOT EXISTS "${GSTREAMER_PLUGIN_PATH}" OR NOT EXISTS "${GSTREAMER_INCLUDE_PATH}") + message(FATAL_ERROR "GStreamer: Could not locate required directories - check installation or set GStreamer_ROOT_DIR") +endif() + +if(GStreamer_USE_FRAMEWORK AND NOT EXISTS "${GSTREAMER_FRAMEWORK_PATH}") + message(FATAL_ERROR "GStreamer: Could not locate framework at ${GSTREAMER_FRAMEWORK_PATH}") +endif() + +# Sub-libraries used directly by QGC source code. Plugin transitive deps +# are resolved automatically from each plugin's .pc file. +if(NOT DEFINED GSTREAMER_EXTRA_DEPS) + set(GSTREAMER_EXTRA_DEPS + gstreamer-base-1.0 + gstreamer-gl-1.0 + gstreamer-gl-prototypes-1.0 + gstreamer-rtsp-1.0 + gstreamer-video-1.0 + ) + if(WIN32) + list(APPEND GSTREAMER_EXTRA_DEPS graphene-1.0) + endif() + if(ANDROID OR IOS) + list(APPEND GSTREAMER_EXTRA_DEPS gio-2.0) + endif() + if(ANDROID) + list(APPEND GSTREAMER_EXTRA_DEPS gmodule-2.0 zlib) + endif() +endif() + +if(NOT DEFINED GSTREAMER_PLUGINS) + set(GSTREAMER_PLUGINS + coreelements + isomp4 + libav + matroska + mpegtsdemux + opengl + openh264 + playback + rtp + rtpmanager + rtsp + sdpelem + tcp + typefindfunctions + udp + videoconvertscale + videoparsersbad + vpx + ) + if(ANDROID) + list(APPEND GSTREAMER_PLUGINS androidmedia dav1d) + elseif(APPLE) + list(APPEND GSTREAMER_PLUGINS applemedia dav1d vulkan) + elseif(WIN32) + list(APPEND GSTREAMER_PLUGINS d3d d3d11 d3d12 dav1d dxva nvcodec) + elseif(LINUX) + list(APPEND GSTREAMER_PLUGINS dav1d nvcodec qsv va vulkan) + endif() +endif() + +if(ANDROID) + set(GStreamer_Mobile_MODULE_NAME gstreamer_android) + set(G_IO_MODULES openssl) + set(G_IO_MODULES_PATH "${GStreamer_ROOT_DIR}/lib/gio/modules") + set(GStreamer_NDK_BUILD_PATH "${GStreamer_ROOT_DIR}/share/gst-android/ndk-build") + set(GStreamer_JAVA_SRC_DIR "${CMAKE_BINARY_DIR}/android-build-${CMAKE_PROJECT_NAME}/src") + set(GStreamer_ASSETS_DIR "${CMAKE_BINARY_DIR}/android-build-${CMAKE_PROJECT_NAME}/assets") +elseif(IOS) + set(GStreamer_Mobile_MODULE_NAME gstreamer_mobile) + set(G_IO_MODULES openssl) + set(G_IO_MODULES_PATH "${GStreamer_ROOT_DIR}/lib/gio/modules") + set(GStreamer_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets") +endif() + +set(GStreamer_ROOT_DIR "${GStreamer_ROOT_DIR}" CACHE PATH "GStreamer SDK root directory" FORCE) + +if(GStreamer_USE_FRAMEWORK) + list(APPEND CMAKE_FRAMEWORK_PATH "${GSTREAMER_FRAMEWORK_PATH}") +endif() + +if(GStreamer_USE_STATIC_LIBS) + list(APPEND PKG_CONFIG_ARGN "--static") +endif() + +find_package(PkgConfig REQUIRED QUIET) + +list(PREPEND CMAKE_PREFIX_PATH ${GStreamer_ROOT_DIR}) +if(LINUX) + pkg_check_modules(PC_GSTREAMER REQUIRED IMPORTED_TARGET gstreamer-1.0>=${GStreamer_FIND_VERSION}) +else() + pkg_check_modules(PC_GSTREAMER REQUIRED IMPORTED_TARGET gstreamer-1.0=${GStreamer_FIND_VERSION}) +endif() +set(GStreamer_VERSION "${PC_GSTREAMER_VERSION}") + +function(find_gstreamer_component component pkgconfig_name) + set(target GStreamer::${component}) + + if(NOT TARGET ${target}) + string(TOUPPER ${component} upper) + pkg_check_modules(PC_GSTREAMER_${upper} IMPORTED_TARGET ${pkgconfig_name}) + if(TARGET PkgConfig::PC_GSTREAMER_${upper}) + qt_add_library(GStreamer::${component} INTERFACE IMPORTED) + target_link_libraries(GStreamer::${component} INTERFACE PkgConfig::PC_GSTREAMER_${upper}) + if(upper STREQUAL "GL") + get_target_property(_qt_incs PkgConfig::PC_GSTREAMER_GL INTERFACE_INCLUDE_DIRECTORIES) + set(__qt_fixed_incs) + foreach(path IN LISTS _qt_incs) + if(IS_DIRECTORY "${path}") + list(APPEND __qt_fixed_incs "${path}") + endif() + endforeach() + set_property(TARGET PkgConfig::PC_GSTREAMER_GL PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${__qt_fixed_incs}") + endif() + endif() + endif() + + if(TARGET ${target}) + set(QGCGStreamer_${component}_FOUND TRUE PARENT_SCOPE) + set(GStreamer_${component}_FOUND TRUE PARENT_SCOPE) + endif() +endfunction() + +find_gstreamer_component(Core gstreamer-1.0) +find_gstreamer_component(Base gstreamer-base-1.0) +find_gstreamer_component(Video gstreamer-video-1.0) +find_gstreamer_component(Gl gstreamer-gl-1.0) +find_gstreamer_component(GlPrototypes gstreamer-gl-prototypes-1.0) +find_gstreamer_component(Rtsp gstreamer-rtsp-1.0) + +if(GlEgl IN_LIST QGCGStreamer_FIND_COMPONENTS) + find_gstreamer_component(GlEgl gstreamer-gl-egl-1.0) +endif() + +if(GlWayland IN_LIST QGCGStreamer_FIND_COMPONENTS) + find_gstreamer_component(GlWayland gstreamer-gl-wayland-1.0) +endif() + +if(GlX11 IN_LIST QGCGStreamer_FIND_COMPONENTS) + find_gstreamer_component(GlX11 gstreamer-gl-x11-1.0) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(QGCGStreamer + REQUIRED_VARS GStreamer_ROOT_DIR GSTREAMER_LIB_PATH GSTREAMER_PLUGIN_PATH + VERSION_VAR GStreamer_VERSION + HANDLE_COMPONENTS +) + +if(QGCGStreamer_FOUND AND NOT TARGET GStreamer::GStreamer) + qt_add_library(GStreamer::GStreamer INTERFACE IMPORTED) + + if(GStreamer_USE_STATIC_LIBS) + target_compile_definitions(GStreamer::GStreamer INTERFACE QGC_GST_STATIC_BUILD) + endif() + + if(APPLE AND GStreamer_USE_FRAMEWORK) + set(_saved_find_framework "${CMAKE_FIND_FRAMEWORK}") + set(CMAKE_FIND_FRAMEWORK ONLY) + cmake_path(GET GSTREAMER_FRAMEWORK_PATH PARENT_PATH _gst_framework_parent) + find_library(GSTREAMER_FRAMEWORK GStreamer + PATHS + "${_gst_framework_parent}" + "${GSTREAMER_FRAMEWORK_PATH}" + "/Library/Frameworks" + "/usr/local/opt/gstreamer" + "/opt/homebrew/opt/gstreamer" + ) + set(CMAKE_FIND_FRAMEWORK "${_saved_find_framework}") + if(GSTREAMER_FRAMEWORK) + target_link_libraries(GStreamer::GStreamer INTERFACE ${GSTREAMER_FRAMEWORK}) + target_include_directories(GStreamer::GStreamer INTERFACE "${GSTREAMER_FRAMEWORK}/Headers") + if(MACOS) + target_compile_definitions(GStreamer::GStreamer INTERFACE QGC_GST_MACOS_FRAMEWORK) + endif() + else() + message(FATAL_ERROR "GStreamer: Could not locate GStreamer.framework") + endif() + + elseif(ANDROID) + target_link_options(GStreamer::GStreamer INTERFACE "-Wl,-Bsymbolic") + if(CMAKE_SIZEOF_VOID_P EQUAL 4) + target_link_options(GStreamer::GStreamer INTERFACE "-Wl,-z,notext") + endif() + endif() + + target_link_directories(GStreamer::GStreamer INTERFACE ${GSTREAMER_LIB_PATH}) + + target_link_libraries(GStreamer::GStreamer + INTERFACE + GStreamer::Core + GStreamer::Base + GStreamer::Video + GStreamer::Gl + GStreamer::GlPrototypes + GStreamer::Rtsp + ) + + foreach(component IN LISTS QGCGStreamer_FIND_COMPONENTS) + if(GStreamer_${component}_FOUND) + target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::${component}) + endif() + endforeach() + + if(GStreamer_USE_STATIC_LIBS) + qt_add_library(GStreamer::Plugins INTERFACE IMPORTED) + target_link_directories(GStreamer::Plugins INTERFACE ${GSTREAMER_PLUGIN_PATH}) + + foreach(plugin IN LISTS GSTREAMER_PLUGINS) + pkg_check_modules(GST_PLUGIN_${plugin} QUIET IMPORTED_TARGET gst${plugin}) + if(GST_PLUGIN_${plugin}_FOUND) + if(TARGET PkgConfig::GST_PLUGIN_${plugin}) + target_link_libraries(GStreamer::Plugins INTERFACE PkgConfig::GST_PLUGIN_${plugin}) + else() + target_link_libraries(GStreamer::Plugins INTERFACE ${GST_PLUGIN_${plugin}_STATIC_LIBRARIES}) + target_link_directories(GStreamer::Plugins INTERFACE ${GST_PLUGIN_${plugin}_STATIC_LIBRARY_DIRS}) + endif() + else() + find_library(GST_PLUGIN_${plugin}_LIBRARY + NAMES gst${plugin} + PATHS + ${GSTREAMER_LIB_PATH} + ${GSTREAMER_PLUGIN_PATH} + NO_DEFAULT_PATH + ) + if(GST_PLUGIN_${plugin}_LIBRARY) + target_link_libraries(GStreamer::Plugins INTERFACE ${GST_PLUGIN_${plugin}_LIBRARY}) + set(GST_PLUGIN_${plugin}_FOUND TRUE) + endif() + endif() + if(GST_PLUGIN_${plugin}_FOUND) + target_compile_definitions(GStreamer::Plugins INTERFACE GST_PLUGIN_${plugin}_FOUND) + endif() + endforeach() + + target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::Plugins) + endif() +endif() diff --git a/cmake/find-modules/GStreamerHelpers.cmake b/cmake/find-modules/GStreamerHelpers.cmake new file mode 100644 index 000000000000..d4a7f97c4a06 --- /dev/null +++ b/cmake/find-modules/GStreamerHelpers.cmake @@ -0,0 +1,410 @@ +# gstreamer_get_package_url( ) +# PLATFORM: android, ios, macos, macos_devel, windows_msvc_x64, windows_msvc_arm64, +# good_plugins, good_plugins_qt6, monorepo +function(gstreamer_get_package_url PLATFORM VERSION OUTPUT_VAR) + if(PLATFORM STREQUAL "android") + set(_url "https://gstreamer.freedesktop.org/data/pkg/android/${VERSION}/gstreamer-1.0-android-universal-${VERSION}.tar.xz") + elseif(PLATFORM STREQUAL "ios") + set(_url "https://gstreamer.freedesktop.org/data/pkg/ios/${VERSION}/gstreamer-1.0-devel-${VERSION}-ios-universal.pkg") + elseif(PLATFORM STREQUAL "macos") + set(_url "https://gstreamer.freedesktop.org/data/pkg/macos/${VERSION}/gstreamer-1.0-${VERSION}-universal.pkg") + elseif(PLATFORM STREQUAL "macos_devel") + set(_url "https://gstreamer.freedesktop.org/data/pkg/macos/${VERSION}/gstreamer-1.0-devel-${VERSION}-universal.pkg") + elseif(PLATFORM STREQUAL "windows_msvc_x64") + set(_url "https://gstreamer.freedesktop.org/data/pkg/windows/${VERSION}/msvc/gstreamer-1.0-msvc-x86_64-${VERSION}.exe") + elseif(PLATFORM STREQUAL "windows_msvc_arm64") + set(_url "https://gstreamer.freedesktop.org/data/pkg/windows/${VERSION}/msvc/gstreamer-1.0-msvc-arm64-${VERSION}.exe") + elseif(PLATFORM STREQUAL "good_plugins") + set(_url "https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-${VERSION}.tar.xz") + elseif(PLATFORM STREQUAL "good_plugins_qt6") + set(_url "https://gitlab.freedesktop.org/gstreamer/gstreamer/-/archive/${VERSION}/gstreamer-${VERSION}.tar.gz?path=subprojects/gst-plugins-good/ext/qt6") + elseif(PLATFORM STREQUAL "monorepo") + set(_url "https://gitlab.freedesktop.org/gstreamer/gstreamer/-/archive/${VERSION}/gstreamer-${VERSION}.tar.gz") + else() + message(FATAL_ERROR "gstreamer_get_package_url: Unknown platform '${PLATFORM}'") + endif() + + set(${OUTPUT_VAR} "${_url}" PARENT_SCOPE) +endfunction() + +# gstreamer_get_s3_mirror_url( ) +# Derives an S3 mirror URL from the primary URL's filename. +# Returns empty string for platforms without an S3 mirror. +function(gstreamer_get_s3_mirror_url PLATFORM VERSION OUTPUT_VAR) + set(_s3_base "https://qgroundcontrol.s3.us-west-2.amazonaws.com/dependencies/gstreamer") + + if(PLATFORM STREQUAL "android") + set(_dir "android") + elseif(PLATFORM STREQUAL "ios") + set(_dir "ios") + elseif(PLATFORM STREQUAL "macos" OR PLATFORM STREQUAL "macos_devel") + set(_dir "macos") + elseif(PLATFORM STREQUAL "windows_msvc_x64" OR PLATFORM STREQUAL "windows_msvc_arm64") + set(_dir "windows") + else() + set(${OUTPUT_VAR} "" PARENT_SCOPE) + return() + endif() + + gstreamer_get_package_url(${PLATFORM} ${VERSION} _primary_url) + # Works for simple URLs without query strings (those platforms are excluded above) + cmake_path(GET _primary_url FILENAME _filename) + + set(${OUTPUT_VAR} "${_s3_base}/${_dir}/${_filename}" PARENT_SCOPE) +endfunction() + +# gstreamer_get_fallback_checksum( ) +# Returns a pinned checksum for known artifacts when sidecar checksum fetch fails. +# This preserves integrity verification during transient checksum endpoint outages. +function(gstreamer_get_fallback_checksum PLATFORM VERSION OUTPUT_VAR) + set(_checksum "") + + if(VERSION STREQUAL "1.28.0") + if(PLATFORM STREQUAL "android") + set(_checksum "SHA256=3315be90b32b96aea5339f161725b4298f939cc502ac299eff81b3819d4f5cc3") + elseif(PLATFORM STREQUAL "windows_msvc_x64") + set(_checksum "SHA256=d6aca5785cae9d9ed447db491cc921163ff6af0c657eb733293c55416c8b71a6") + elseif(PLATFORM STREQUAL "windows_msvc_arm64") + set(_checksum "SHA256=99c41b2a4db08730cf17d89f17fed8eddb81e5da569285c83e00266a1f1b9357") + elseif(PLATFORM STREQUAL "macos") + set(_checksum "SHA256=9c252ae9d3ac5bc54505c4a4e93556c7d6e93218a18ad8060b30770d6db036a6") + elseif(PLATFORM STREQUAL "macos_devel") + set(_checksum "SHA256=592d6fe925799470a67da9cbbba6badf4d7ac63c89f6f4532baad7ec1daba5a4") + elseif(PLATFORM STREQUAL "ios") + set(_checksum "SHA256=c4e3365e37c3eae05d8acd470d338560c89a213fb58157c8773db151dc7ebcd7") + elseif(PLATFORM STREQUAL "good_plugins") + set(_checksum "SHA256=d97700f346fdf9ef5461c035e23ed1ce916ca7a31d6ddad987f774774361db77") + endif() + endif() + + set(${OUTPUT_VAR} "${_checksum}" PARENT_SCOPE) +endfunction() + +# gstreamer_fetch_checksum( ) +# Downloads the .sha256sum sidecar from freedesktop.org and returns "SHA256=". +# Returns empty string on failure (checksum unavailable or unparseable). +function(_gstreamer_is_release_like_config OUTPUT_VAR) + set(_release_like FALSE) + + if(CMAKE_CONFIGURATION_TYPES) + foreach(_cfg IN LISTS CMAKE_CONFIGURATION_TYPES) + if(_cfg MATCHES "^(Release|RelWithDebInfo|MinSizeRel)$") + set(_release_like TRUE) + break() + endif() + endforeach() + elseif(CMAKE_BUILD_TYPE MATCHES "^(Release|RelWithDebInfo|MinSizeRel)$") + set(_release_like TRUE) + endif() + + set(${OUTPUT_VAR} "${_release_like}" PARENT_SCOPE) +endfunction() + +function(gstreamer_fetch_checksum PLATFORM VERSION OUTPUT_VAR) + gstreamer_get_package_url(${PLATFORM} ${VERSION} _pkg_url) + + # URLs with query strings (e.g. good_plugins_qt6) have no sidecar checksum + string(FIND "${_pkg_url}" "?" _qs_pos) + if(NOT _qs_pos EQUAL -1) + _gstreamer_is_release_like_config(_require_checksum) + if(_require_checksum) + message(FATAL_ERROR + "GStreamer: Checksums are required for release-like builds, but ${PLATFORM} ${VERSION} " + "uses a query-string URL without sidecar checksum support.\n" + "Use a source with a pinned EXPECTED_HASH.") + endif() + message(STATUS "GStreamer: Skipping checksum for ${PLATFORM} ${VERSION} (URL contains query string)") + set(${OUTPUT_VAR} "" PARENT_SCOPE) + return() + endif() + + set(_checksum_url "${_pkg_url}.sha256sum") + + string(MD5 _url_hash "${_checksum_url}") + set(_checksum_dir "${CMAKE_BINARY_DIR}/_deps/checksums") + set(_checksum_file "${_checksum_dir}/${_url_hash}.sha256sum") + + # Try up to 2 rounds: if a cached file fails to parse, delete and re-download. + foreach(_round RANGE 1 2) + if(NOT EXISTS "${_checksum_file}") + file(MAKE_DIRECTORY "${_checksum_dir}") + set(_checksum_tmp "${_checksum_file}.tmp") + set(_code 1) + foreach(_attempt RANGE 1 3) + file(DOWNLOAD "${_checksum_url}" "${_checksum_tmp}" + STATUS _status + TIMEOUT 30 + TLS_VERIFY ON + ) + list(GET _status 0 _code) + if(_code EQUAL 0) + break() + endif() + list(GET _status 1 _msg) + message(STATUS "GStreamer: Checksum download attempt ${_attempt}/3 failed: ${_msg}") + file(REMOVE "${_checksum_tmp}") + endforeach() + if(NOT _code EQUAL 0) + gstreamer_get_fallback_checksum(${PLATFORM} ${VERSION} _fallback_hash) + if(_fallback_hash) + message(WARNING + "GStreamer: Checksum sidecar unavailable for ${PLATFORM} ${VERSION}; " + "using pinned fallback checksum.") + set(${OUTPUT_VAR} "${_fallback_hash}" PARENT_SCOPE) + return() + endif() + _gstreamer_is_release_like_config(_require_checksum) + if(_require_checksum) + message(FATAL_ERROR "GStreamer: Checksum not available for ${PLATFORM} ${VERSION} — " + "integrity verification is required for release builds") + endif() + message(WARNING + "GStreamer: Checksum sidecar unavailable for ${PLATFORM} ${VERSION}.\n" + "Add a pinned fallback checksum to GStreamerHelpers.cmake for verified builds.") + set(${OUTPUT_VAR} "" PARENT_SCOPE) + return() + endif() + file(RENAME "${_checksum_tmp}" "${_checksum_file}") + endif() + + file(READ "${_checksum_file}" _content) + # Strip surrounding whitespace and any non-hex prefix (e.g. UTF-8 BOM). + string(STRIP "${_content}" _content) + string(REGEX MATCH "([0-9a-fA-F]+)" _match "${_content}") + if(_match) + string(LENGTH "${CMAKE_MATCH_1}" _hash_len) + if(_hash_len EQUAL 64) + set(${OUTPUT_VAR} "SHA256=${CMAKE_MATCH_1}" PARENT_SCOPE) + return() + endif() + endif() + + # Parse failed — purge cached file and retry once + file(SIZE "${_checksum_file}" _file_size) + string(SUBSTRING "${_content}" 0 60 _content_preview) + string(REGEX REPLACE "[^ -~]" "?" _content_preview "${_content_preview}") + message(STATUS "GStreamer: Could not parse checksum for ${PLATFORM} ${VERSION} " + "(file size: ${_file_size}, preview: [${_content_preview}])") + file(REMOVE "${_checksum_file}") + endforeach() + + gstreamer_get_fallback_checksum(${PLATFORM} ${VERSION} _fallback_hash) + if(_fallback_hash) + message(WARNING + "GStreamer: Sidecar checksum content invalid/unparseable for ${PLATFORM} ${VERSION}; " + "using pinned fallback checksum.") + set(${OUTPUT_VAR} "${_fallback_hash}" PARENT_SCOPE) + return() + endif() + + _gstreamer_is_release_like_config(_require_checksum) + if(_require_checksum) + message(FATAL_ERROR + "GStreamer: Could not parse checksum for ${PLATFORM} ${VERSION} after retry " + "(invalid sidecar content). URL: ${_checksum_url}") + endif() + message(STATUS "GStreamer: Could not parse checksum for ${PLATFORM} ${VERSION} (invalid content)") + set(${OUTPUT_VAR} "" PARENT_SCOPE) +endfunction() + +# gstreamer_resilient_download(URLS ... FILENAME DESTINATION_DIR +# RESULT_VAR [TIMEOUT ] [INACTIVITY_TIMEOUT ] [EXPECTED_HASH ] +# [ALLOW_FAILURE]) +# Downloads a file, trying each URL in order. Uses atomic temp-file rename to +# prevent partial-download ghosts. Skips if the destination file already exists. +macro(_gstreamer_parse_expected_hash _HASH_SPEC _OUT_ALGO _OUT_EXPECTED) + set(_gph_valid MD5 SHA1 SHA224 SHA256 SHA384 SHA512 SHA3_224 SHA3_256 SHA3_384 SHA3_512) + string(REGEX MATCH "^([A-Za-z0-9_]+)=(.+)$" _gph_match "${${_HASH_SPEC}}") + if(NOT _gph_match) + message(FATAL_ERROR "gstreamer_resilient_download: Invalid EXPECTED_HASH format '${${_HASH_SPEC}}' (expected ALGO=hex)") + endif() + set(${_OUT_ALGO} "${CMAKE_MATCH_1}") + set(${_OUT_EXPECTED} "${CMAKE_MATCH_2}") + list(FIND _gph_valid "${${_OUT_ALGO}}" _gph_idx) + if(_gph_idx EQUAL -1) + message(FATAL_ERROR "gstreamer_resilient_download: Unsupported hash algorithm '${${_OUT_ALGO}}'") + endif() +endmacro() + +function(gstreamer_resilient_download) + cmake_parse_arguments(ARG "ALLOW_FAILURE" "FILENAME;DESTINATION_DIR;RESULT_VAR;TIMEOUT;INACTIVITY_TIMEOUT;EXPECTED_HASH" "URLS" ${ARGN}) + + foreach(_required FILENAME DESTINATION_DIR RESULT_VAR) + if(NOT ARG_${_required}) + message(FATAL_ERROR "gstreamer_resilient_download: ${_required} is required") + endif() + endforeach() + if(NOT ARG_URLS) + message(FATAL_ERROR "gstreamer_resilient_download: at least one URL is required") + endif() + + if(NOT ARG_TIMEOUT) + set(ARG_TIMEOUT 120) + endif() + if(NOT ARG_INACTIVITY_TIMEOUT) + set(ARG_INACTIVITY_TIMEOUT 60) + endif() + + set(_dest "${ARG_DESTINATION_DIR}/${ARG_FILENAME}") + + if(EXISTS "${_dest}") + if(ARG_EXPECTED_HASH) + _gstreamer_parse_expected_hash(ARG_EXPECTED_HASH _hash_algo _expected) + file(${_hash_algo} "${_dest}" _cached_hash) + if(NOT _cached_hash STREQUAL "${_expected}") + message(STATUS "GStreamer: Cached ${ARG_FILENAME} failed ${_hash_algo} check, re-downloading") + file(REMOVE "${_dest}") + else() + set(${ARG_RESULT_VAR} "${_dest}" PARENT_SCOPE) + return() + endif() + else() + message(STATUS "GStreamer: Using cached ${ARG_FILENAME} (no checksum verification)") + set(${ARG_RESULT_VAR} "${_dest}" PARENT_SCOPE) + return() + endif() + endif() + + file(MAKE_DIRECTORY "${ARG_DESTINATION_DIR}") + set(_tmp "${_dest}.tmp") + set(_tried_urls "") + + foreach(_url IN LISTS ARG_URLS) + if(NOT _url) + continue() + endif() + message(STATUS "GStreamer: Downloading ${ARG_FILENAME} from ${_url}") + file(DOWNLOAD "${_url}" "${_tmp}" + STATUS _status + SHOW_PROGRESS + TIMEOUT ${ARG_TIMEOUT} + INACTIVITY_TIMEOUT ${ARG_INACTIVITY_TIMEOUT} + TLS_VERIFY ON + ) + list(GET _status 0 _code) + if(_code EQUAL 0) + if(ARG_EXPECTED_HASH) + _gstreamer_parse_expected_hash(ARG_EXPECTED_HASH _hash_algo _expected) + file(${_hash_algo} "${_tmp}" _actual_hash) + if(NOT _actual_hash STREQUAL "${_expected}") + message(WARNING "GStreamer: ${_hash_algo} mismatch for ${ARG_FILENAME} from ${_url}") + file(REMOVE "${_tmp}") + list(APPEND _tried_urls "${_url}") + continue() + endif() + endif() + file(RENAME "${_tmp}" "${_dest}") + set(${ARG_RESULT_VAR} "${_dest}" PARENT_SCOPE) + return() + endif() + list(GET _status 1 _error) + message(WARNING "GStreamer: Failed to download from ${_url}: ${_error}") + file(REMOVE "${_tmp}") + list(APPEND _tried_urls "${_url}") + endforeach() + + if(ARG_ALLOW_FAILURE) + message(STATUS "GStreamer: All download URLs failed for ${ARG_FILENAME} (allowing failure). Tried: ${_tried_urls}") + set(${ARG_RESULT_VAR} "" PARENT_SCOPE) + return() + endif() + + message(FATAL_ERROR "GStreamer: All download URLs failed for ${ARG_FILENAME}.\n" + "Tried: ${_tried_urls}\n" + "Install manually from https://gstreamer.freedesktop.org/download/ or set GStreamer_ROOT_DIR.") +endfunction() + +# gstreamer_get_recommended_version( []) +# Patch versions are auto-extracted from platform version strings in build-config.json +# via BuildConfig.cmake (e.g., gstreamer_android_version "1.22.12" -> QGC_GSTREAMER_PATCH_1_22=12). +function(gstreamer_get_recommended_version MAJOR MINOR OUTPUT_VAR) + set(_var_name "QGC_GSTREAMER_PATCH_${MAJOR}_${MINOR}") + if(DEFINED ${_var_name}) + set(_patch "${${_var_name}}") + elseif(ARGC GREATER 3 AND NOT "${ARGV3}" STREQUAL "") + set(_patch "${ARGV3}") + message(STATUS + "No patch mapping for GStreamer ${MAJOR}.${MINOR}; using detected patch ${_patch}.") + else() + set(_patch 0) + message(WARNING "No patch mapping for GStreamer ${MAJOR}.${MINOR} — defaulting to .0. " + "This version is not used by any platform target in .github/build-config.json.") + endif() + + set(${OUTPUT_VAR} "${MAJOR}.${MINOR}.${_patch}" PARENT_SCOPE) +endfunction() + +function(gstreamer_install_gio_modules) + cmake_parse_arguments(ARG "" "SOURCE_DIR;DEST_DIR;EXTENSION" "" ${ARGN}) + + if(NOT EXISTS "${ARG_SOURCE_DIR}") + message(WARNING "gstreamer_install_gio_modules: SOURCE_DIR does not exist: ${ARG_SOURCE_DIR}") + return() + endif() + + # Ship the full GIO module set from the selected runtime. Filtering can + # remove TLS/proxy helpers required by some deployments. + file(GLOB modules_to_install "${ARG_SOURCE_DIR}/*.${ARG_EXTENSION}*") + + if(modules_to_install) + install(FILES ${modules_to_install} DESTINATION "${ARG_DEST_DIR}") + endif() +endfunction() + +function(gstreamer_install_plugins) + cmake_parse_arguments(ARG "" "SOURCE_DIR;DEST_DIR;EXTENSION;PREFIX" "" ${ARGN}) + + if(NOT EXISTS "${ARG_SOURCE_DIR}") + message(WARNING "gstreamer_install_plugins: SOURCE_DIR does not exist: ${ARG_SOURCE_DIR}") + return() + endif() + + file(GLOB all_plugins "${ARG_SOURCE_DIR}/${ARG_PREFIX}*.${ARG_EXTENSION}*") + + # Install only plugins listed in GSTREAMER_PLUGINS (set by FindQGCGStreamer). + set(plugins_to_install "") + foreach(plugin_path IN LISTS all_plugins) + get_filename_component(plugin_name "${plugin_path}" NAME) + foreach(allowed IN LISTS GSTREAMER_PLUGINS) + if(plugin_name MATCHES "^${ARG_PREFIX}${allowed}([^a-zA-Z0-9]|$)") + list(APPEND plugins_to_install "${plugin_path}") + break() + endif() + endforeach() + endforeach() + + if(plugins_to_install) + install(FILES ${plugins_to_install} DESTINATION "${ARG_DEST_DIR}") + endif() +endfunction() + +# _gst_normalize_and_validate_root() +# Normalize GStreamer_ROOT_DIR path separators and verify the directory exists. +# Must be called as a macro (not function) so it modifies the caller's scope. +macro(_gst_normalize_and_validate_root) + cmake_path(CONVERT "${GStreamer_ROOT_DIR}" TO_CMAKE_PATH_LIST GStreamer_ROOT_DIR NORMALIZE) + if(NOT EXISTS "${GStreamer_ROOT_DIR}") + message(FATAL_ERROR "GStreamer: SDK not found at '${GStreamer_ROOT_DIR}' — " + "check installation or set GStreamer_ROOT_DIR") + endif() +endmacro() + +# WARNING: This function copies ALL shared libraries from SOURCE_DIR without +# filtering. Only call it for auto-downloaded SDKs or known-clean directories. +# Do NOT point it at a shared system prefix (e.g. /usr/lib, Homebrew lib/). +function(gstreamer_install_libs) + cmake_parse_arguments(ARG "" "SOURCE_DIR;DEST_DIR;EXTENSION" "" ${ARGN}) + + if(NOT EXISTS "${ARG_SOURCE_DIR}") + message(WARNING "gstreamer_install_libs: SOURCE_DIR does not exist: ${ARG_SOURCE_DIR}") + return() + endif() + + # The GStreamer SDK ships only GStreamer and its transitive dependencies — + # there are no unrelated libraries to exclude. Maintaining a prefix allowlist + # would be fragile and silently break when new dependencies appear. + file(GLOB all_libs "${ARG_SOURCE_DIR}/*.${ARG_EXTENSION}") + if(all_libs) + install(FILES ${all_libs} DESTINATION "${ARG_DEST_DIR}") + endif() +endfunction() diff --git a/cmake/find-modules/_FindGStreamer.cmake b/cmake/find-modules/_FindGStreamer.cmake deleted file mode 100644 index 8d858bed8238..000000000000 --- a/cmake/find-modules/_FindGStreamer.cmake +++ /dev/null @@ -1,456 +0,0 @@ -# SPDX-FileCopyrightText: 2024 L. E. Segovia -# SPDX-License-Ref: LGPL-2.1-or-later - -#[=======================================================================[.rst: -FindGStreamer -------- - -Finds the GStreamer library. Requires ``pkg-config`` to be installed. - -Configuration -^^^^^^^^^^^^^ - -This module can be configured with the following variables: - -``GStreamer_STATIC`` - Link against GStreamer statically (see below). - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` targets: - -``GStreamer::GStreamer`` - The GStreamer library. - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables: - -``GStreamer_FOUND`` - True if the system has the GStreamer library. -``GStreamer_VERSION`` - The version of the GStreamer library which was found. -``GStreamer_INCLUDE_DIRS`` - Include directories needed to use GStreamer. -``GStreamer_LIBRARIES`` - Libraries needed to link to GStreamer. - -Cache Variables -^^^^^^^^^^^^^^^ - -The following cache variables may also be set: - -``GStreamer_INCLUDE_DIR`` - The directory containing ``gst/gstversion.h``. -``GStreamer_LIBRARY`` - The path to the GStreamer library. - -Configuration Variables -^^^^^^^^^^^^^^^ - -Setting the following variables is required, depending on the operating system: - -``GStreamer_ROOT_DIR`` - Installation prefix of the GStreamer SDK. - -``GStreamer_USE_STATIC_LIBS` - Set to ON to force the use of the static libraries. Default is OFF. - -``GStreamer_EXTRA_DEPS`` - pkg-config names of the extra dependencies that will be included whenever linking against GStreamer. - -#]=======================================================================] - -if (GStreamer_FOUND) - return() -endif() - -##################### -# Setup variables # -##################### - -if (NOT DEFINED GStreamer_ROOT_DIR AND DEFINED GSTREAMER_ROOT) - set(GStreamer_ROOT_DIR ${GSTREAMER_ROOT}) -endif() - -if (NOT GStreamer_ROOT_DIR) - set(GStreamer_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../../") -endif() - -if (NOT EXISTS "${GStreamer_ROOT_DIR}") - message(FATAL_ERROR "The directory GStreamer_ROOT_DIR=${GStreamer_ROOT_DIR} does not exist") -endif() - -if (NOT DEFINED GStreamer_USE_STATIC_LIBS) -set(GStreamer_USE_STATIC_LIBS OFF) -endif() - -# Set the environment for pkg-config -if (WIN32) - set(ENV{PKG_CONFIG_PATH} "${GStreamer_ROOT_DIR}/lib/pkgconfig;${GStreamer_ROOT_DIR}/lib/gstreamer-1.0/pkgconfig;${GStreamer_ROOT_DIR}/lib/gio/modules/pkgconfig") -else() - set(ENV{PKG_CONFIG_PATH} "${GStreamer_ROOT_DIR}/lib/pkgconfig:${GStreamer_ROOT_DIR}/lib/gstreamer-1.0/pkgconfig:${GStreamer_ROOT_DIR}/lib/gio/modules/pkgconfig") -endif() - -# Set the list of extra dependencies -if (NOT DEFINED GStreamer_EXTRA_DEPS) - set(GStreamer_EXTRA_DEPS) - if (DEFINED GSTREAMER_EXTRA_DEPS) - set(GStreamer_EXTRA_DEPS ${GSTREAMER_EXTRA_DEPS}) - endif() -endif() - -# Find libraries. This is meant to be used with static libraries -# (hence the reprioritization) but I've added a fallback to shared libraries -# and stub modules in case any are non-existent. -function(_gst_find_library LOCAL_LIB GST_LOCAL_LIB) - if (DEFINED ${GST_LOCAL_LIB}) - return() - endif() - - set(_gst_suffixes ${CMAKE_FIND_LIBRARY_SUFFIXES}) - set(_gst_prefixes ${CMAKE_FIND_LIBRARY_PREFIXES}) - if (APPLE) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".dylib" ".so" ".tbd") - set(CMAKE_FIND_LIBRARY_PREFIXES "" "lib") - elseif (UNIX) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".so") - set(CMAKE_FIND_LIBRARY_PREFIXES "" "lib") - else() - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".lib") - set(CMAKE_FIND_LIBRARY_PREFIXES "" "lib") - endif() - - if ("${LOCAL_LIB}" IN_LIST _gst_IGNORED_SYSTEM_LIBRARIES) - set(${GST_LOCAL_LIB} ${LOCAL_LIB} PARENT_SCOPE) - else() - find_library(${GST_LOCAL_LIB} - ${LOCAL_LIB} - HINTS ${ARGN} - NO_DEFAULT_PATH - NO_CMAKE_FIND_ROOT_PATH - REQUIRED - ) - set(${GST_LOCAL_LIB} "${${GST_LOCAL_LIB}}" PARENT_SCOPE) - if (NOT ${GST_LOCAL_LIB}) - message(FATAL_ERROR "${LOCAL_LIB} was unexpectedly not found.") - endif() - endif() - - set(CMAKE_FIND_LIBRARY_SUFFIXES ${_gst_suffixes}) - set(CMAKE_FIND_LIBRARY_PREFIXES ${_gst_prefixes}) -endfunction() - -macro(_gst_apply_link_libraries HIDE PC_LIBRARIES PC_HINTS GST_TARGET) - if (APPLE AND ${HIDE}) - target_link_directories(${GST_TARGET} INTERFACE - ${${PC_HINTS}} - ) - endif() - foreach(LOCAL_LIB IN LISTS ${PC_LIBRARIES}) - if (LOCAL_LIB MATCHES "${_gst_SRT_REGEX_PATCH}") - string(REGEX REPLACE "${_gst_SRT_REGEX_PATCH}" "\\1" LOCAL_LIB "${LOCAL_LIB}") - endif() - string(MAKE_C_IDENTIFIER "_gst_${LOCAL_LIB}" GST_LOCAL_LIB) - if (NOT ${GST_LOCAL_LIB}) - _gst_find_library(${LOCAL_LIB} ${GST_LOCAL_LIB} ${${PC_HINTS}}) - endif() - if ("${${GST_LOCAL_LIB}}" IN_LIST _gst_IGNORED_SYSTEM_LIBRARIES) - target_link_libraries(${GST_TARGET} INTERFACE - ${${GST_LOCAL_LIB}}) - elseif (APPLE AND ${HIDE}) - set(LOCAL_FILE) - get_filename_component(LOCAL_FILE ${${GST_LOCAL_LIB}} NAME) - target_link_libraries(${GST_TARGET} INTERFACE - "-hidden-l${LOCAL_FILE}") - elseif((UNIX OR ANDROID) AND ${HIDE}) - target_link_libraries(${GST_TARGET} INTERFACE - -Wl,--exclude-libs,${${GST_LOCAL_LIB}}) - else() - target_link_libraries(${GST_TARGET} INTERFACE - ${${GST_LOCAL_LIB}}) - endif() - endforeach() -endmacro() - -macro(_gst_filter_missing_directories GST_INCLUDE_DIRS) - set(_gst_include_dirs) - foreach(DIR IN LISTS ${GST_INCLUDE_DIRS}) - string(MAKE_C_IDENTIFIER "${DIR}" _gst_dir_id) - if (DEFINED _gst_exists_${_gst_dir_id}) - if (_gst_exists_${_gst_dir_id}) - list(APPEND _gst_include_dirs "${DIR}") - endif() - elseif (EXISTS "${DIR}") - list(APPEND _gst_include_dirs "${DIR}") - set(_gst_exists_${_gst_dir_id} TRUE) - else() - message(WARNING "Skipping missing include folder ${DIR}.") - set(_gst_exists_${_gst_dir_id} FALSE) - endif() - endforeach() - set(${GST_INCLUDE_DIRS} "${_gst_include_dirs}") -endmacro() - -macro(_gst_apply_frameworks PC_STATIC_LDFLAGS_OTHER GST_TARGET) - if (APPLE) - # LDFLAGS_OTHER may include framework linkage. Because CMake - # iterates over arguments separated by spaces, it doesn't realise - # that those arguments must not be split. - set(new_ldflags) - set(assemble_framework FALSE) - foreach(_arg IN LISTS ${PC_STATIC_LDFLAGS_OTHER}) - if (assemble_framework) - set(assemble_framework FALSE) - find_library(GST_${_arg}_LIB ${_arg} REQUIRED) - target_link_libraries(${GST_TARGET} - INTERFACE - "${GST_${_arg}_LIB}" - ) - elseif (_arg STREQUAL "-framework") - set(assemble_framework TRUE) - else() - set(assemble_framework FALSE) - list(APPEND new_ldflags "${_arg}") - endif() - endforeach() - set_target_properties(${GST_TARGET} PROPERTIES - INTERFACE_LINK_OPTIONS "${new_ldflags}" - ) - else() - set_target_properties(${TARGET} PROPERTIES - INTERFACE_LINK_OPTIONS "${${PC_STATIC_LDFLAGS_OTHER}}" - ) - endif() -endmacro() - -################################ -# Set up the targets # -################################ - -find_package(PkgConfig REQUIRED) - -# GStreamer's pkg-config modules are a MUST -- but we'll test them below -pkg_check_modules(PC_GStreamer gstreamer-1.0 ${GStreamer_EXTRA_DEPS}) -# Simulate the list that'll be wholearchive'd. -# Unfortunately, this uses an option only available with pkgconf. -# set(_old_pkg_config_executable "${PKG_CONFIG_EXECUTABLE}") -# set(PKG_CONFIG_EXECUTABLE ${PKG_CONFIG_EXECUTABLE} --maximum-traverse-depth=1) -# pkg_check_modules(PC_GStreamer_NoDeps QUIET REQUIRED gstreamer-1.0 ${GStreamer_EXTRA_DEPS}) -# set(PKG_CONFIG_EXECUTABLE "${_old_pkg_config_executable}") - -set(GStreamer_VERSION "${PC_GStreamer_VERSION}") - -# Test validity of the paths -# NOTE: only paths that must be considered are those provided by pkg-config -# NOTE 2: also exclude sysroots -find_path(GStreamer_INCLUDE_DIR - NAMES gst/gstversion.h - PATHS ${PC_GStreamer_INCLUDE_DIRS} - PATH_SUFFIXES gstreamer-1.0 - NO_DEFAULT_PATH - NO_CMAKE_FIND_ROOT_PATH - REQUIRED -) - -find_library(GStreamer_LIBRARY - NAMES gstreamer-1.0 - PATHS ${PC_GStreamer_LIBRARY_DIRS} - NO_DEFAULT_PATH - NO_CMAKE_FIND_ROOT_PATH - REQUIRED -) - -# Android: Ignore these libraries when constructing the IMPORTED_LOCATION -set(_gst_IGNORED_SYSTEM_LIBRARIES c c++ unwind m dl atomic) -if (ANDROID) - list(APPEND _gst_IGNORED_SYSTEM_LIBRARIES log GLESv2 EGL OpenSLES android vulkan) -elseif(APPLE) - list(APPEND _gst_IGNORED_SYSTEM_LIBRARIES iconv resolv System) -endif() - -# Normalize library flags coming from srt/haisrt -# https://github.com/Haivision/srt/commit/b90b64d26f850fb0efcc4cdd8b31cbf74bd4db0c -set(_gst_SRT_REGEX_PATCH "^:lib(.+)\\.(a|so|lib|dylib)$") - -if(PC_GStreamer_FOUND AND (NOT TARGET GStreamer::GStreamer)) - # This is not UNKNOWN but INTERFACE, as we only intend to - # make a target suitable for downstream consumption. - # FindPkgConfig already takes care of things, however it is totally unable - # to discern between shared and static libraries when populating - # xxx_STATIC_LINK_LIBRARIES, so we need to populate them manually. - add_library(GStreamer::GStreamer INTERFACE IMPORTED) - - if (GStreamer_USE_STATIC_LIBS) - _gst_filter_missing_directories(PC_GStreamer_STATIC_INCLUDE_DIRS) - set_target_properties(GStreamer::GStreamer PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_GStreamer_STATIC_CFLAGS_OTHER}" - ) - if (PC_GStreamer_STATIC_INCLUDE_DIRS) - set_target_properties(GStreamer::GStreamer PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${PC_GStreamer_STATIC_INCLUDE_DIRS}" - ) - endif() - _gst_apply_frameworks(PC_GStreamer_STATIC_LDFLAGS_OTHER GStreamer::GStreamer) - else() - set_target_properties(GStreamer::GStreamer PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_GStreamer_CFLAGS_OTHER}" - INTERFACE_INCLUDE_DIRECTORIES "${PC_GStreamer_INCLUDE_DIRS}" - INTERFACE_LINK_OPTIONS "${PC_GStreamer_LDFLAGS_OTHER}" - ) - endif() - - add_library(GStreamer::deps INTERFACE IMPORTED) - - if (NOT GStreamer_USE_STATIC_LIBS) - set_target_properties(GStreamer::deps PROPERTIES - INTERFACE_LINK_LIBRARIES "${PC_GStreamer_LINK_LIBRARIES}" - ) - # We're done - else() - # Handle all libraries, even those specified with -l:libfoo.a (srt) - # Due to the unavailability of pkgconf's `--maximum-traverse-depth` - # on stock pkg-config, I attempt to simulate it through the shared - # libraries listing. - # If pkgconf is available, replace all PC_GStreamer_ entries with - # PC_GStreamer_NoDeps and uncomment the code block above. - foreach(LOCAL_LIB IN LISTS PC_GStreamer_LIBRARIES) - # list(TRANSFORM REPLACE) is of no use here - # https://gitlab.kitware.com/cmake/cmake/-/issues/16899 - if (LOCAL_LIB MATCHES "${_gst_SRT_REGEX_PATCH}") - string(REGEX REPLACE "${_gst_SRT_REGEX_PATCH}" "\\1" LOCAL_LIB "${LOCAL_LIB}") - endif() - string(MAKE_C_IDENTIFIER "_gst_${LOCAL_LIB}" GST_LOCAL_LIB) - if (NOT ${GST_LOCAL_LIB}) - _gst_find_library(${LOCAL_LIB} ${GST_LOCAL_LIB} ${PC_GStreamer_STATIC_LIBRARY_DIRS}) - endif() - target_link_libraries(GStreamer::GStreamer INTERFACE - "${${GST_LOCAL_LIB}}" - ) - endforeach() - - _gst_apply_link_libraries(ON PC_GStreamer_STATIC_LIBRARIES PC_GStreamer_STATIC_LIBRARY_DIRS GStreamer::deps) - endif() - - target_link_libraries(GStreamer::GStreamer - INTERFACE - GStreamer::deps - ) -endif() - -foreach(_gst_PLUGIN IN LISTS GSTREAMER_PLUGINS) - # Safety valve for the custom targets above - if ("${_gst_plugin}" IN_LIST _gst_CUSTOM_TARGETS) - continue() - endif() - - if (TARGET GStreamer::${_gst_PLUGIN}) - continue() - endif() - - if (GStreamer_FIND_REQUIRED_${_gst_PLUGIN}) - set(_gst_PLUGIN_REQUIRED REQUIRED) - else() - set(_gst_PLUGIN_REQUIRED) - endif() - - pkg_check_modules(PC_GStreamer_${_gst_PLUGIN} "gst${_gst_PLUGIN}") - - set(GStreamer_${_gst_PLUGIN}_FOUND "${PC_GStreamer_${_gst_PLUGIN}_FOUND}") - if (NOT GStreamer_${_gst_PLUGIN}_FOUND) - continue() - endif() - - add_library(GStreamer::${_gst_PLUGIN} INTERFACE IMPORTED) - _gst_filter_missing_directories(PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS) - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_CFLAGS_OTHER}" - ) - if (PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS) - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS}" - ) - endif() - if (GStreamer_USE_STATIC_LIBS) - _gst_apply_frameworks(PC_GStreamer_${_gst_PLUGIN}_STATIC_LDFLAGS_OTHER GStreamer::${_gst_PLUGIN}) - else() - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_LINK_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_LDFLAGS_OTHER}" - INTERFACE_LINK_LIBRARIES "${PC_GStreamer_${_gst_PLUGIN}_LINK_LIBRARIES}" - ) - # We're done - continue() - endif() - - # Handle all libraries, even those specified with -l:libfoo.a (srt) - _gst_apply_link_libraries(OFF PC_GStreamer_${_gst_PLUGIN}_STATIC_LIBRARIES PC_GStreamer_${_gst_PLUGIN}_STATIC_LIBRARY_DIRS GStreamer::${_gst_PLUGIN}) -endforeach() - -foreach(_gst_PLUGIN IN LISTS GSTREAMER_APIS) - # Safety valve for the custom targets above - if ("${_gst_plugin}" IN_LIST _gst_CUSTOM_TARGETS) - continue() - endif() - - if (TARGET GStreamer::${_gst_PLUGIN}) - continue() - endif() - - if (GStreamer_FIND_REQUIRED_${_gst_PLUGIN}) - set(_gst_PLUGIN_REQUIRED REQUIRED) - else() - set(_gst_PLUGIN_REQUIRED) - endif() - - string(REGEX REPLACE "^api_(.+)" "\\1" _gst_PLUGIN_PC "${_gst_PLUGIN}") - string(REPLACE "_" "-" _gst_PLUGIN_PC "${_gst_PLUGIN_PC}") - - pkg_check_modules(PC_GStreamer_${_gst_PLUGIN} "gstreamer-${_gst_PLUGIN_PC}-1.0") - - set(GStreamer_${_gst_PLUGIN}_FOUND "${PC_GStreamer_${_gst_PLUGIN}_FOUND}") - if (NOT GStreamer_${_gst_PLUGIN}_FOUND) - continue() - endif() - - add_library(GStreamer::${_gst_PLUGIN} INTERFACE IMPORTED) - _gst_filter_missing_directories(PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS) - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_CFLAGS_OTHER}" - INTERFACE_LINK_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_LDFLAGS_OTHER}" - ) - if (PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS) - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS}" - ) - endif() - if (GStreamer_USE_STATIC_LIBS) - _gst_apply_frameworks(PC_GStreamer_${_gst_PLUGIN}_STATIC_LDFLAGS_OTHER GStreamer::${_gst_PLUGIN}) - else() - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_LINK_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_LDFLAGS_OTHER}" - INTERFACE_LINK_LIBRARIES "${PC_GStreamer_${_gst_PLUGIN}_LINK_LIBRARIES}" - ) - # We're done - continue() - endif() - - # Handle all libraries, even those specified with -l:libfoo.a (srt) - _gst_apply_link_libraries(OFF PC_GStreamer_${_gst_PLUGIN}_STATIC_LIBRARIES PC_GStreamer_${_gst_PLUGIN}_STATIC_LIBRARY_DIRS GStreamer::${_gst_PLUGIN}) -endforeach() - -# Perform final validation -include(FindPackageHandleStandardArgs) -set(_gst_handle_version_range) -if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.19.0") - set(_gst_handle_version_range "HANDLE_VERSION_RANGE") -endif() -find_package_handle_standard_args(GStreamer - REQUIRED_VARS - GStreamer_LIBRARY - GStreamer_INCLUDE_DIR - VERSION_VAR GStreamer_VERSION - ${_gst_handle_version_range} - HANDLE_COMPONENTS -) diff --git a/cmake/find-modules/_FindGStreamerMobile.cmake b/cmake/find-modules/_FindGStreamerMobile.cmake deleted file mode 100644 index 90311b27428e..000000000000 --- a/cmake/find-modules/_FindGStreamerMobile.cmake +++ /dev/null @@ -1,607 +0,0 @@ -# SPDX-FileCopyrightText: 2024 L. E. Segovia -# SPDX-License-Ref: LGPL-2.1-or-later - -#[=======================================================================[.rst: -FindGStreamerMobile -------- - -Creates additional mobile targets to install fonts and the CA certificate -bundle. Android and iOS only. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`INTERFACE` targets: - -``GStreamer::fonts`` - A target that will install GStreamer's default fonts into the app. - -``GStreamer::ca_certificates`` - A target that will install the NSS CA certificate bundle into the app. - -This module defines the following :prop_tgt:`SHARED` targets: - -``GStreamer::mobile`` - A target that will build the shared library consisting of GStreamer plus all the selected plugin components. (Android/iOS only) - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables: - -``GStreamer_Mobile_FOUND`` - ON if the system has the GStreamer library. - -Cache Variables -^^^^^^^^^^^^^^^ - -The following cache variables may also be set: - -``GStreamer_CA_BUNDLE`` - Path to /etc/ssl/certs/ca-certificates.crt. -``GStreamer_UBUNTU_R_TTF`` - Path to the TrueType font Ubuntu R. -``GStreamer_FONTS_CONF`` - Path to /etc/fonts.conf. - -Configuration Variables -^^^^^^^^^^^^^^^ - -Like with the main GStreamer library, setting the following variables is -required, depending on the operating system: - -``GStreamer_ROOT_DIR`` - Installation prefix of the GStreamer SDK. - -``GStreamer_JAVA_SRC_DIR`` - Target directory for deploying the selected plugins' Java classfiles to. (Android only) - -``GStreamer_Mobile_MODULE_NAME`` - Name for the GStreamer::mobile shared library. Default is ``gstreamer_android`` (Android) or ``gstreamer_mobile`` (iOS). - -``GStreamer_ASSETS_DIR`` - Target directory for deploying assets to. - -``G_IO_MODULES`` - Set this to the GIO modules you need, additional to any GStreamer plugins. (Usually set to ``gnutls`` or ``openssl``) - -``G_IO_MODULES_PATH`` - Path for the static GIO modules. - -#]=======================================================================] - -if (GStreamer_Mobile_FOUND) - return() -endif() - -##################### -# Setup variables # -##################### - -if (NOT DEFINED GStreamer_ROOT_DIR AND DEFINED GSTREAMER_ROOT) - set(GStreamer_ROOT_DIR ${GSTREAMER_ROOT}) -endif() - -if (NOT GStreamer_ROOT_DIR) - set(GStreamer_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../../") -endif() - -if (NOT EXISTS "${GStreamer_ROOT_DIR}") - message(FATAL_ERROR "The directory GStreamer_ROOT_DIR=${GStreamer_ROOT_DIR} does not exist") -endif() - -set(_gst_required_vars) - -if (ca_certificates IN_LIST GStreamerMobile_FIND_COMPONENTS) - # for setting the default GTlsDatabase - list(APPEND GStreamer_EXTRA_DEPS gio-2.0) -endif() - -if (ANDROID) - list(APPEND GStreamer_EXTRA_DEPS zlib) -endif() - -# Prepare Android hotfixes for x264 -if(ANDROID_ABI MATCHES "^armeabi") - set(NEEDS_NOTEXT_FIX TRUE) - set(NEEDS_BSYMBOLIC_FIX TRUE) -elseif(ANDROID_ABI STREQUAL "x86") - set(NEEDS_NOTEXT_FIX TRUE) - set(NEEDS_BSYMBOLIC_FIX TRUE) -# arm64: https://ffmpeg.org/pipermail/ffmpeg-devel/2022-July/298734.html -elseif(ANDROID_ABI STREQUAL "x86_64" OR ANDROID_ABI STREQUAL "arm64-v8a") - set(NEEDS_BSYMBOLIC_FIX TRUE) -endif() - -# Set up output variables for Android -if(ANDROID) - if (NOT DEFINED GStreamer_JAVA_SRC_DIR AND DEFINED GSTREAMER_JAVA_SRC_DIR) - set(GStreamer_JAVA_SRC_DIR ${GSTREAMER_JAVA_SRC_DIR}) - elseif(NOT DEFINED GStreamer_JAVA_SRC_DIR) - set(GStreamer_JAVA_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../src/") - else() - # Gradle does not let us access the root of the subproject - # so we implement the ndk-build assumption ourselves - set(GStreamer_JAVA_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../${GStreamer_JAVA_SRC_DIR}") - endif() - - if(NOT DEFINED GStreamer_NDK_BUILD_PATH AND DEFINED GSTREAMER_NDK_BUILD_PATH) - set(GStreamer_NDK_BUILD_PATH "${GSTREAMER_NDK_BUILD_PATH}") - elseif(NOT DEFINED GStreamer_NDK_BUILD_PATH) - set(GStreamer_NDK_BUILD_PATH "${GStreamer_ROOT}/share/gst-android/ndk-build/") - endif() -endif() - -if(NOT DEFINED GStreamer_Mobile_MODULE_NAME) - if (DEFINED GSTREAMER_ANDROID_MODULE_NAME) - set(GStreamer_Mobile_MODULE_NAME "${GSTREAMER_ANDROID_MODULE_NAME}") - elseif(ANDROID) - set(GStreamer_Mobile_MODULE_NAME gstreamer_android) - else() - set(GStreamer_Mobile_MODULE_NAME gstreamer_mobile) - endif() -endif() - -if(ANDROID) - if(NOT DEFINED GStreamer_ASSETS_DIR AND DEFINED GSTREAMER_ASSETS_DIR) - set(GStreamer_ASSETS_DIR "${GSTREAMER_ASSETS_DIR}") - elseif(NOT DEFINED GStreamer_ASSETS_DIR) - set(GStreamer_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../src/assets/") - else() - # Same as above - set(GStreamer_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../${GStreamer_ASSETS_DIR}") - endif() - - if(NOT DEFINED GStreamer_NDK_BUILD_PATH AND DEFINED GSTREAMER_NDK_BUILD_PATH) - set(GStreamer_NDK_BUILD_PATH "${GSTREAMER_NDK_BUILD_PATH}") - elseif(NOT DEFINED GStreamer_NDK_BUILD_PATH) - set(GStreamer_NDK_BUILD_PATH "${GStreamer_ROOT}/share/gst-android/ndk-build/") - endif() -elseif(IOS) - if(NOT DEFINED GStreamer_ASSETS_DIR AND DEFINED GStreamer_ASSETS_DIR) - set(GStreamer_ASSETS_DIR ${GStreamer_ASSETS_DIR}) - elseif(NOT DEFINED GStreamer_ASSETS_DIR) - set(GStreamer_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets") - else() - # Same as above - set(GStreamer_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../${GStreamer_ASSETS_DIR}") - endif() -endif() - -if (ANDROID) - file(READ "${GStreamer_NDK_BUILD_PATH}/GStreamer.java" JAVA_INPUT) -endif() - -if (ANDROID OR APPLE) - set(GSTREAMER_IS_MOBILE ON) -else() - set(GSTREAMER_IS_MOBILE OFF) -endif() - -# Block shared GStreamer on mobile -if (GSTREAMER_IS_MOBILE) - if (NOT DEFINED GStreamer_USE_STATIC_LIBS) - set(GStreamer_USE_STATIC_LIBS ON) - endif() - if (NOT GStreamer_USE_STATIC_LIBS) - message(FATAL_ERROR "Shared library GStreamer is not supported on mobile platforms") - endif() -endif() - -# Now, let's set up targets for each of the components supplied -# These are the required plugins -set(_gst_plugins ${GStreamerMobile_FIND_COMPONENTS}) -# These are custom handled targets, and must be skipped from the loop -list(REMOVE_ITEM _gst_plugins fonts ca_certificates mobile) -list(REMOVE_DUPLICATES _gst_plugins) - -set(GSTREAMER_PLUGINS ${_gst_plugins}) -# These are the API packages -set(GSTREAMER_APIS ${GSTREAMER_PLUGINS}) -list(FILTER GSTREAMER_APIS INCLUDE REGEX "^api_") -# Filter them out, although they're handled the same -# they cannot be considered for the purposes of initialization -list(FILTER GSTREAMER_PLUGINS EXCLUDE REGEX "^api_") - -if (GSTREAMER_IS_MOBILE AND (NOT TARGET GStreamer::mobile)) - # Generate the plugins' declaration strings - # (don't append a semicolon, CMake does it as part of the list) - list(TRANSFORM GSTREAMER_PLUGINS - PREPEND "\nGST_PLUGIN_STATIC_DECLARE\(" - OUTPUT_VARIABLE PLUGINS_DECLARATION - ) - list(TRANSFORM PLUGINS_DECLARATION - APPEND "\)" - OUTPUT_VARIABLE PLUGINS_DECLARATION - ) - if(PLUGINS_DECLARATION) - set(PLUGINS_DECLARATION "${PLUGINS_DECLARATION};") - endif() - - # Generate the plugins' registration strings - list(TRANSFORM GSTREAMER_PLUGINS - PREPEND "\nGST_PLUGIN_STATIC_REGISTER\(" - OUTPUT_VARIABLE PLUGINS_REGISTRATION - ) - list(TRANSFORM PLUGINS_REGISTRATION - APPEND "\)" - OUTPUT_VARIABLE PLUGINS_REGISTRATION - ) - if(PLUGINS_REGISTRATION) - set(PLUGINS_REGISTRATION "${PLUGINS_REGISTRATION};") - endif() - - # Generate list of gio modules - if (NOT G_IO_MODULES) - set(G_IO_MODULES) - endif() - list(TRANSFORM G_IO_MODULES - PREPEND "gio" - OUTPUT_VARIABLE G_IO_MODULES_LIBS - ) - list(TRANSFORM G_IO_MODULES - PREPEND "\nGST_G_IO_MODULE_DECLARE\(" - OUTPUT_VARIABLE G_IO_MODULES_DECLARE - ) - list(TRANSFORM G_IO_MODULES_DECLARE - APPEND "\);" - OUTPUT_VARIABLE G_IO_MODULES_DECLARE - ) - if(G_IO_MODULES_DECLARE) - set(G_IO_MODULES_DECLARE "${G_IO_MODULES_DECLARE};") - endif() - list(TRANSFORM G_IO_MODULES - PREPEND "\nGST_G_IO_MODULE_LOAD\(" - OUTPUT_VARIABLE G_IO_MODULES_LOAD - ) - list(TRANSFORM G_IO_MODULES_LOAD - APPEND "\)" - OUTPUT_VARIABLE G_IO_MODULES_LOAD - ) - if(G_IO_MODULES_LOAD) - set(G_IO_MODULES_LOAD "${G_IO_MODULES_LOAD};") - endif() - - # Generates a source file that declares and registers all the required plugins - if (ANDROID) - configure_file( - "${CMAKE_CURRENT_LIST_DIR}/GStreamer/gstreamer_android-1.0.c.in" - "${GStreamer_Mobile_MODULE_NAME}.c" - ) - else() - configure_file( - "${CMAKE_CURRENT_LIST_DIR}/GStreamer/gst_ios_init.m.in" - "${GStreamer_Mobile_MODULE_NAME}.m" - ) - endif() - - # Creates a shared library including gstreamer, its plugins and all the dependencies - if (ANDROID) - add_library(GStreamerMobile - SHARED - "${GStreamer_Mobile_MODULE_NAME}.c" - ) - else() - add_library(GStreamerMobile SHARED) - enable_language(OBJC OBJCXX) - target_sources(GStreamerMobile - PRIVATE - "${GStreamer_Mobile_MODULE_NAME}.m" - ) - set_source_files_properties("${GStreamer_Mobile_MODULE_NAME}.m" - PROPERTIES - LANGUAGE OBJC - ) - find_library(Foundation_LIB Foundation REQUIRED) - target_link_libraries(GStreamerMobile - PRIVATE - ${Foundation_LIB} - ) - endif() - add_library(GStreamer::mobile ALIAS GStreamerMobile) - - # Assume it's C++ for the sake of gstsoundtouch - set_target_properties( - GStreamerMobile - PROPERTIES - LIBRARY_OUTPUT_NAME ${GStreamer_Mobile_MODULE_NAME} - ) - if (APPLE) - set_target_properties( - GStreamerMobile - PROPERTIES - LINKER_LANGUAGE OBJCXX - FRAMEWORK TRUE - FRAMEWORK_VERSION A - MACOSX_FRAMEWORK_IDENTIFIER org.gstreamer.GStreamerMobile - ) - else() - set_target_properties( - GStreamerMobile - PROPERTIES - LINKER_LANGUAGE CXX - ) - endif() -endif() - -if (GStreamerMobile_FIND_REQUIRED) -find_package(GStreamer COMPONENTS ${_gst_plugins} REQUIRED) -else() -find_package(GStreamer COMPONENTS ${_gst_plugins}) -endif() - -# Path for the static GIO modules -pkg_get_variable(G_IO_MODULES_PATH gio-2.0 giomoduledir) -if (NOT G_IO_MODULES_PATH) - set(G_IO_MODULES_PATH "${GStreamer_ROOT_DIR}/lib/gio/modules") -endif() - -if (GSTREAMER_IS_MOBILE) - set_target_properties( - GStreamerMobile - PROPERTIES - VERSION ${PC_GStreamer_VERSION} - SOVERSION ${PC_GStreamer_VERSION} - ) - - # Handle all libraries, even those specified with -l:libfoo.a (srt) - # Due to the unavailability of pkgconf's `--maximum-traverse-depth` - # on stock pkg-config, I attempt to simulate it through the shared - # libraries listing. - # If pkgconf is available, replace all PC_GStreamer_ entries with - # PC_GStreamer_NoDeps and uncomment the code block above. - foreach(LOCAL_LIB IN LISTS PC_GStreamer_LIBRARIES) - # list(TRANSFORM REPLACE) is of no use here - # https://gitlab.kitware.com/cmake/cmake/-/issues/16899 - if (LOCAL_LIB MATCHES "${_gst_SRT_REGEX_PATCH}") - string(REGEX REPLACE "${_gst_SRT_REGEX_PATCH}" "\\1" LOCAL_LIB "${LOCAL_LIB}") - endif() - string(MAKE_C_IDENTIFIER "_gst_${LOCAL_LIB}" GST_LOCAL_LIB) - # These have already been found by FindGStreamer - if ("${${GST_LOCAL_LIB}}" IN_LIST _gst_IGNORED_SYSTEM_LIBRARIES) - target_link_libraries(GStreamerMobile PRIVATE - "${${GST_LOCAL_LIB}}" - ) - elseif (MSVC) - target_link_libraries(GStreamerMobile PRIVATE - "/WHOLEARCHIVE:${${GST_LOCAL_LIB}}" - ) - elseif(APPLE) - target_link_libraries(GStreamerMobile PRIVATE - "-Wl,-force_load,${${GST_LOCAL_LIB}}" - ) - else() - target_link_libraries(GStreamerMobile PRIVATE - "-Wl,--whole-archive,${${GST_LOCAL_LIB}},--no-whole-archive" - ) - endif() - endforeach() - - target_link_libraries( - GStreamerMobile - PRIVATE - GStreamer::deps - ) - - target_link_options( - GStreamerMobile - INTERFACE - $ - ) - - target_include_directories( - GStreamerMobile - INTERFACE - $ - ) - - # Text relocations are required for all 32-bit objects. We - # must disable the warning to allow linking with lld. Unlike gold, ld which - # will silently allow text relocations, lld support must be explicit. - # - # See https://crbug.com/911658#c19 for more information. See also - # https://trac.ffmpeg.org/ticket/7878 - if(DEFINED NEEDS_NOTEXT_FIX) - target_link_options( - GStreamerMobile - PRIVATE - "-Wl,-z,notext" - ) - endif() - - # resolve textrels in the x86 asm - if(DEFINED NEEDS_BSYMBOLIC_FIX) - target_link_options( - GStreamerMobile - PRIVATE - "-Wl,-Bsymbolic" - ) - endif() - - if (ANDROID) - # Collect all Java-based initializer classes - set(GSTREAMER_PLUGINS_CLASSES) - foreach(LOCAL_PLUGIN IN LISTS GSTREAMER_PLUGINS) - file(GLOB_RECURSE - LOCAL_PLUGIN_CLASS - FOLLOW_SYMLINKS - RELATIVE "${GStreamer_NDK_BUILD_PATH}" - "${GStreamer_NDK_BUILD_PATH}/${LOCAL_PLUGIN}/*.java" - ) - list(APPEND GSTREAMER_PLUGINS_CLASSES ${LOCAL_PLUGIN_CLASS}) - endforeach() - - # Same as above, but collect the plugins themselves - set(GSTREAMER_PLUGINS_WITH_CLASSES) - foreach(LOCAL_PLUGIN IN LISTS GSTREAMER_PLUGINS) - if(EXISTS "${GStreamer_NDK_BUILD_PATH}/${LOCAL_PLUGIN}/") - list(APPEND GSTREAMER_PLUGINS_WITH_CLASSES ${LOCAL_PLUGIN}) - endif() - endforeach() - - add_custom_target( - "copyjavasource_${ANDROID_ABI}" - ) - - foreach(LOCAL_FILE IN LISTS GSTREAMER_PLUGINS_CLASSES) - string(MAKE_C_IDENTIFIER "cp_${LOCAL_FILE}" COPYJAVASOURCE_TGT) - add_custom_target( - ${COPYJAVASOURCE_TGT} - COMMAND - "${CMAKE_COMMAND}" -E make_directory - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${GStreamer_NDK_BUILD_PATH}/${LOCAL_FILE}" - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/" - BYPRODUCTS - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/${LOCAL_FILE}" - ) - add_dependencies(copyjavasource_${ANDROID_ABI} ${COPYJAVASOURCE_TGT}) - endforeach() - endif() - - # And, finally, set the GIO modules up - if (G_IO_MODULES_LIBS) - add_library(GStreamer::gio_modules INTERFACE IMPORTED) - - _gst_apply_link_libraries(OFF G_IO_MODULES_LIBS G_IO_MODULES_PATH GStreamer::gio_modules) - target_link_libraries( - GStreamerMobile - PRIVATE - GStreamer::gio_modules - ) - endif() - set(GStreamer_mobile_FOUND TRUE) -endif() - -if(fonts IN_LIST GStreamerMobile_FIND_COMPONENTS) - set(GStreamer_UBUNTU_R_TTF "${GStreamer_NDK_BUILD_PATH}/fontconfig/fonts/Ubuntu-R.ttf" - CACHE FILEPATH "Path to Ubuntu-R.ttf" - ) - set(GStreamer_FONTS_CONF - "${GStreamer_NDK_BUILD_PATH}/fontconfig/fonts.conf" - CACHE FILEPATH "Path to fonts.conf" - ) - if (EXISTS "${GStreamer_UBUNTU_R_TTF}" AND EXISTS "${GStreamer_FONTS_CONF}") - set(GStreamerMobile_fonts_FOUND ON) - set(_gst_required_vars "GStreamer_UBUNTU_R_TTF GStreamer_FONTS_CONF ${gst_required_vars}") - - if (ANDROID) - string(REPLACE "//copyFonts" "copyFonts" JAVA_INPUT "${JAVA_INPUT}") - add_custom_target( - copyfontsres_${ANDROID_ABI} - COMMAND - "${CMAKE_COMMAND}" -E make_directory - "${GStreamer_ASSETS_DIR}/fontconfig/fonts/truetype/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${GStreamer_UBUNTU_R_TTF}" - "${GStreamer_ASSETS_DIR}/fontconfig/fonts/truetype/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${GStreamer_FONTS_CONF}" - "${GStreamer_ASSETS_DIR}/fontconfig/" - BYPRODUCTS - "${GStreamer_ASSETS_DIR}/fontconfig/fonts/truetype/Ubuntu-R.ttf" - "${GStreamer_ASSETS_DIR}/fontconfig/fonts.conf" - ) - - if (TARGET GStreamerMobile) - add_dependencies(GStreamerMobile copyfontsres_${ANDROID_ABI}) - endif() - elseif(APPLE) - set(GStreamerMobile_fonts_FOUND ON) - list(APPEND GSTREAMER_RESOURCES "${GStreamer_FONTS_CONF}" "${GStreamer_UBUNTU_R_TTF}") - else() - message(FATAL_ERROR "No fonts assets available for this operating system.") - endif() - else() - set(GStreamerMobile_fonts_FOUND OFF) - endif() -endif() - -if(ca_certificates IN_LIST GStreamerMobile_FIND_COMPONENTS) - set(GStreamer_CA_BUNDLE "${GStreamer_ROOT_DIR}/etc/ssl/certs/ca-certificates.crt" - CACHE FILEPATH "Path to ca-certificates bundle" - ) - if (EXISTS "${GStreamer_CA_BUNDLE}") - set(GStreamerMobile_ca_certificates_FOUND ON) - string(REPLACE "//copyCaCertificates" "copyCaCertificates" JAVA_INPUT "${JAVA_INPUT}") - set(_gst_required_vars "GStreamer_CA_BUNDLE ${_gst_required_vars}") - - if (ANDROID) - add_custom_target( - copycacertificatesres_${ANDROID_ABI} - COMMAND - "${CMAKE_COMMAND}" -E make_directory - "${GStreamer_ASSETS_DIR}/ssl/certs/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${GStreamer_CA_BUNDLE}" - "${GStreamer_ASSETS_DIR}/ssl/certs/" - BYPRODUCTS "${GStreamer_ASSETS_DIR}/ssl/certs/ca-certificates.crt" - ) - - if (TARGET GStreamerMobile) - add_dependencies(GStreamerMobile copycacertificatesres_${ANDROID_ABI}) - target_compile_definitions(GStreamerMobile - PRIVATE - GSTREAMER_INCLUDE_CA_CERTIFICATES - ) - endif() - elseif (APPLE) - list(APPEND GSTREAMER_RESOURCES "${GStreamer_CA_BUNDLE}") - else() - message(FATAL_ERROR "No certificate bundle available for this operating system.") - endif() - else() - set(GStreamerMobile_ca_certificates_FOUND OFF) - endif() -endif() - -if (ANDROID) - file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/GStreamer.java" "${JAVA_INPUT}") - add_custom_target( - enable_includes_in_gstreamer_java - COMMAND - "${CMAKE_COMMAND}" -E make_directory - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${CMAKE_CURRENT_BINARY_DIR}/GStreamer.java" - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/GStreamer.java" - BYPRODUCTS - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/GStreamer.java" - ) - if (TARGET GStreamerMobile) - add_dependencies(copyjavasource_${ANDROID_ABI} enable_includes_in_gstreamer_java) - add_dependencies(GStreamerMobile copyjavasource_${ANDROID_ABI}) - endif() -endif() - -if (TARGET GStreamerMobile AND GSTREAMER_RESOURCES) - set_target_properties( - GStreamerMobile - PROPERTIES - RESOURCE "${GSTREAMER_RESOURCES}" - ) -endif() - -# Perform final validation -include(FindPackageHandleStandardArgs) -foreach(_gst_PLUGIN IN LISTS _gst_plugins) - set(GStreamerMobile_${_gst_PLUGIN}_FOUND "${GStreamer_${_gst_PLUGIN}_FOUND}") - - if (GStreamer_${_gst_PLUGIN}_FOUND) - target_link_libraries( - GStreamerMobile - PRIVATE - GStreamer::${_gst_PLUGIN} - ) - endif() -endforeach() -# FIXME: CMake does not tolerate interpolation of REQUIRED_VARS -find_package_handle_standard_args(GStreamerMobile - HANDLE_COMPONENTS -) diff --git a/deploy/docker/entrypoint.sh b/deploy/docker/entrypoint.sh index 9514ce8243a0..2d8098cc8b68 100644 --- a/deploy/docker/entrypoint.sh +++ b/deploy/docker/entrypoint.sh @@ -27,7 +27,6 @@ if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then -DQT_HOST_PATH="${QT_HOST_PATH}" \ -DQT_ANDROID_ABIS="${ANDROID_ABIS}" \ -DANDROID_SDK_ROOT="${ANDROID_SDK_ROOT}" \ - -DQGC_CUSTOM_GST_PACKAGE=OFF \ -DQT_ANDROID_SIGN_APK=OFF cmake --build /project/build --target all --config "${BUILD_TYPE}" --parallel else diff --git a/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt b/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt index 057db82665df..244b3277c556 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt +++ b/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt @@ -1,37 +1,29 @@ -# ============================================================================ -# GStreamer Video Receiver Backend -# GStreamer-based video streaming and decoding -# ============================================================================ - target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_sources(${CMAKE_PROJECT_NAME} PRIVATE VideoItemStub.h) -# ============================================================================ -# GStreamer Detection and Configuration -# ============================================================================ - if(QGC_ENABLE_GST_VIDEOSTREAMING) - if(NOT MACOS) - # NOTE: Using FindGStreamer.cmake is currently bypassed on macOS - # Using hardwired framework path as a workaround until FindGStreamer works on macOS - find_package(GStreamer - REQUIRED - COMPONENTS Core Base Video Gl GlPrototypes Rtsp - OPTIONAL_COMPONENTS GlEgl GlWayland GlX11 - ) + find_package(QGCGStreamer + REQUIRED + COMPONENTS Core Base Video Gl GlPrototypes Rtsp + OPTIONAL_COMPONENTS GlEgl GlWayland GlX11 + ) + + # Expose GST_PLUGIN__FOUND for each discovered plugin so that + # GStreamer.cc can conditionally register static plugin declarations. + if(GStreamer_USE_STATIC_LIBS) + foreach(_plugin IN LISTS GSTREAMER_PLUGINS) + if(GST_PLUGIN_${_plugin}_FOUND) + target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE GST_PLUGIN_${_plugin}_FOUND) + endif() + endforeach() endif() - # Build GStreamer Qt6 QML GL plugin add_subdirectory(gstqml6gl) # TODO: Add Qt6 Direct3D11 plugin support # https://gstreamer.freedesktop.org/documentation/qt6d3d11/index.html#qml6d3d11sink-page endif() -# ============================================================================ -# GStreamer Video Receiver Sources -# ============================================================================ - if(TARGET gstqml6gl) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE gstqml6gl) @@ -45,60 +37,67 @@ if(TARGET gstqml6gl) GstVideoReceiver.h ) - # Build custom GStreamer QGC plugin add_subdirectory(gstqgc) target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE QGC_GST_STREAMING) - # ============================================================================ - # Platform-Specific GStreamer Installation - # ============================================================================ - if(LINUX) - # ---------------------------------------------------------------------------- - # Linux: Install GStreamer Plugins and Libraries - # ---------------------------------------------------------------------------- - install( - DIRECTORY + # Fixed "lib" to match AppRun expectations (CMAKE_INSTALL_LIBDIR varies by distro) + gstreamer_install_plugins( + SOURCE_DIR "${GSTREAMER_PLUGIN_PATH}" + DEST_DIR "lib/gstreamer-1.0" + EXTENSION "so" + PREFIX "libgst" + ) + gstreamer_install_gio_modules( + SOURCE_DIR "${GSTREAMER_LIB_PATH}/gio/modules" + DEST_DIR "lib/gio/modules" + EXTENSION "so" + ) + # Helper binary path varies by distro: Debian lib//gstreamer1.0/, + # Fedora libexec/gstreamer-1.0/, Arch lib/gstreamer-1.0/. + # We install to Debian convention (lib/gstreamer1.0/gstreamer-1.0/); + # runtime env var GST_PLUGIN_SCANNER in GStreamer.cc matches this path. + find_program(_GST_PLUGIN_SCANNER gst-plugin-scanner + PATHS + "${GSTREAMER_LIB_PATH}/gstreamer1.0/gstreamer-1.0" + "${GStreamer_ROOT_DIR}/libexec/gstreamer-1.0" "${GSTREAMER_PLUGIN_PATH}" - "${GSTREAMER_LIB_PATH}/gio" - DESTINATION "${CMAKE_INSTALL_LIBDIR}" - FILES_MATCHING - PATTERN "*.so" - PATTERN "*.so.*" - PATTERN "*/include" EXCLUDE + NO_DEFAULT_PATH ) - install( - DIRECTORY "${GSTREAMER_LIB_PATH}/gstreamer1.0" - DESTINATION "${CMAKE_INSTALL_LIBDIR}" - FILE_PERMISSIONS - OWNER_READ OWNER_WRITE OWNER_EXECUTE - GROUP_READ GROUP_EXECUTE - WORLD_READ WORLD_EXECUTE - FILES_MATCHING - PATTERN "gst-*" + if(_GST_PLUGIN_SCANNER) + install(PROGRAMS "${_GST_PLUGIN_SCANNER}" + DESTINATION "lib/gstreamer1.0/gstreamer-1.0") + else() + message(WARNING "gst-plugin-scanner not found; AppImage video may not work") + endif() + find_program(_GST_PTP_HELPER gst-ptp-helper + PATHS + "${GSTREAMER_LIB_PATH}/gstreamer1.0/gstreamer-1.0" + "${GStreamer_ROOT_DIR}/libexec/gstreamer-1.0" + "${GSTREAMER_PLUGIN_PATH}" + NO_DEFAULT_PATH ) + if(_GST_PTP_HELPER) + install(PROGRAMS "${_GST_PTP_HELPER}" + DESTINATION "lib/gstreamer1.0/gstreamer-1.0") + endif() elseif(WIN32) - # ---------------------------------------------------------------------------- - # Windows: Install GStreamer DLLs and Plugins - # ---------------------------------------------------------------------------- - install( - DIRECTORY "${GStreamer_ROOT_DIR}/bin/" - DESTINATION "${CMAKE_INSTALL_BINDIR}" - FILES_MATCHING - PATTERN "*.dll" + gstreamer_install_libs( + SOURCE_DIR "${GStreamer_ROOT_DIR}/bin" + DEST_DIR "${CMAKE_INSTALL_BINDIR}" + EXTENSION "dll" ) - install( - DIRECTORY "${GSTREAMER_LIB_PATH}/gio/modules/" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/gio/modules" - FILES_MATCHING - PATTERN "*.dll" + gstreamer_install_gio_modules( + SOURCE_DIR "${GSTREAMER_LIB_PATH}/gio/modules" + DEST_DIR "${CMAKE_INSTALL_LIBDIR}/gio/modules" + EXTENSION "dll" ) - install( - DIRECTORY "${GSTREAMER_PLUGIN_PATH}/" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/gstreamer-1.0" - FILES_MATCHING - PATTERN "*.dll" + gstreamer_install_plugins( + SOURCE_DIR "${GSTREAMER_PLUGIN_PATH}" + DEST_DIR "${CMAKE_INSTALL_LIBDIR}/gstreamer-1.0" + EXTENSION "dll" + PREFIX "gst" ) install( DIRECTORY "${GStreamer_ROOT_DIR}/libexec/gstreamer-1.0/" @@ -111,13 +110,17 @@ if(TARGET gstqml6gl) PATTERN "*.exe" ) elseif(MACOS) - # ---------------------------------------------------------------------------- - # macOS: Install GStreamer Framework - # ---------------------------------------------------------------------------- + # Framework install preserves internal structure/symlinks required for codesigning. + # Plugin filtering not applied to avoid breaking the framework bundle. if(GSTREAMER_FRAMEWORK) + # Resolve the framework real path before install. Some SDK layouts expose + # the framework as a symlink; copying through the symlink can leave an + # unsigned wrapper path inside the app bundle. + get_filename_component(_gst_framework_real "${GSTREAMER_FRAMEWORK}" REALPATH) install( - DIRECTORY "${GSTREAMER_FRAMEWORK}" - DESTINATION "${CMAKE_INSTALL_PREFIX}/${CMAKE_PROJECT_NAME}.app/Contents/Frameworks" + DIRECTORY "${_gst_framework_real}/" + DESTINATION "${CMAKE_INSTALL_PREFIX}/${CMAKE_PROJECT_NAME}.app/Contents/Frameworks/GStreamer.framework" + USE_SOURCE_PERMISSIONS PATTERN "*.la" EXCLUDE PATTERN "*.a" EXCLUDE PATTERN "*/bin" EXCLUDE @@ -127,7 +130,12 @@ if(TARGET gstqml6gl) PATTERN "*/include" EXCLUDE PATTERN "*/pkgconfig" EXCLUDE PATTERN "*/share" EXCLUDE + PATTERN "*gstpython*" EXCLUDE ) + else() + message(WARNING "macOS: Homebrew GStreamer detected. " + "Distributable builds require the GStreamer.framework SDK from " + "https://gstreamer.freedesktop.org/download/") endif() endif() endif() diff --git a/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.h b/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.h deleted file mode 100644 index 32a385078830..000000000000 --- a/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -void gst_ios_pre_init(void); -void gst_ios_post_init(void); - -G_END_DECLS diff --git a/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.m b/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.m deleted file mode 100644 index 638744391b09..000000000000 --- a/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.m +++ /dev/null @@ -1,80 +0,0 @@ -#include -#include -#include - -/* Declaration of static plugins */ - @PLUGINS_DECLARATION@ - -/* Declaration of static gio modules */ - @G_IO_MODULES_DECLARE@ - -#ifdef GSTREAMER_INCLUDE_CA_CERTIFICATES -static void -gst_ios_load_certificates (const gchar *resources_dir) -{ - gchar *ca_certificates; - - ca_certificates = g_build_filename (resources_dir, "ssl", "certs", "ca-certificates.crt", NULL); - g_setenv ("CA_CERTIFICATES", ca_certificates, TRUE); - - if (ca_certificates) { - GTlsBackend *backend = g_tls_backend_get_default (); - if (backend) { - GTlsDatabase *db = g_tls_file_database_new (ca_certificates, NULL); - if (db) - g_tls_backend_set_default_database (backend, db); - } - } - g_free (ca_certificates); -} - -#endif - -void -gst_ios_init (void) -{ - GstPluginFeature *plugin; - GstRegistry *reg; - NSString *resources = [[NSBundle mainBundle] resourcePath]; - NSString *tmp = NSTemporaryDirectory(); - NSString *cache = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Caches"]; - NSString *docs = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"]; - - const gchar *resources_dir = [resources UTF8String]; - const gchar *tmp_dir = [tmp UTF8String]; - const gchar *cache_dir = [cache UTF8String]; - const gchar *docs_dir = [docs UTF8String]; - - g_setenv ("TMP", tmp_dir, TRUE); - g_setenv ("TEMP", tmp_dir, TRUE); - g_setenv ("TMPDIR", tmp_dir, TRUE); - g_setenv ("XDG_RUNTIME_DIR", resources_dir, TRUE); - g_setenv ("XDG_CACHE_HOME", cache_dir, TRUE); - - g_setenv ("HOME", docs_dir, TRUE); - g_setenv ("XDG_DATA_DIRS", resources_dir, TRUE); - g_setenv ("XDG_CONFIG_DIRS", resources_dir, TRUE); - g_setenv ("XDG_CONFIG_HOME", cache_dir, TRUE); - g_setenv ("XDG_DATA_HOME", resources_dir, TRUE); - g_setenv ("FONTCONFIG_PATH", resources_dir, TRUE); - - @G_IO_MODULES_LOAD@ - -#ifdef GSTREAMER_INCLUDE_CA_CERTIFICATES - gst_ios_load_certificates (resources_dir); -#endif - - gst_init (NULL, NULL); - - @PLUGINS_REGISTRATION@ - - /* Lower the ranks of filesrc and giosrc so iosavassetsrc is - * tried first in gst_element_make_from_uri() for file:// */ - reg = gst_registry_get(); - plugin = gst_registry_lookup_feature(reg, "filesrc"); - if (plugin) - gst_plugin_feature_set_rank(plugin, GST_RANK_SECONDARY); - plugin = gst_registry_lookup_feature(reg, "giosrc"); - if (plugin) - gst_plugin_feature_set_rank(plugin, GST_RANK_SECONDARY-1); -} diff --git a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt b/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt index c2ff75c707a4..6e75ca44c6c8 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt +++ b/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt @@ -5,32 +5,7 @@ qt_add_library(gstqml6gl STATIC) -# ---------------------------------------------------------------------------- -# Platform-Specific GStreamer Linking -# ---------------------------------------------------------------------------- -if(MACOS) - # macOS uses framework path directly (FindGStreamer.cmake bypassed) - find_library(GSTREAMER_FRAMEWORK GStreamer) - set(GST_REQUIRED_FRAMEWORK_VERSION ${QGC_CONFIG_GSTREAMER_MACOS_VERSION}) - set(GST_PLUGINS_VERSION ${GST_REQUIRED_FRAMEWORK_VERSION}) - set(GSTREAMER_FRAMEWORK_PATH "/Library/Frameworks/GStreamer.framework") - if(NOT EXISTS "${GSTREAMER_FRAMEWORK_PATH}") - message(FATAL_ERROR "GStreamer.framework not found at ${GSTREAMER_FRAMEWORK_PATH}. Install GStreamer using tools/setup/install_dependencies.py --platform macos") - endif() - target_link_libraries(gstqml6gl PUBLIC "$") - target_include_directories(gstqml6gl SYSTEM PUBLIC "${GSTREAMER_FRAMEWORK_PATH}/Headers") - # Verify correct framework version - execute_process( - COMMAND defaults read "${GSTREAMER_FRAMEWORK_PATH}/Versions/1.0/Resources/Info.plist" CFBundleShortVersionString - OUTPUT_VARIABLE GST_INSTALLED_FRAMEWORK_VERSION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(NOT GST_INSTALLED_FRAMEWORK_VERSION STREQUAL GST_REQUIRED_FRAMEWORK_VERSION) - message(FATAL_ERROR "GStreamer.framework version (${GST_INSTALLED_FRAMEWORK_VERSION}) does not match required framework version (${GST_REQUIRED_FRAMEWORK_VERSION}). Please install the correct version using tools/setup/install_dependencies.py --platform macos") - endif() -else() - target_link_libraries(gstqml6gl PUBLIC GStreamer::GStreamer) -endif() +target_link_libraries(gstqml6gl PUBLIC GStreamer::GStreamer) # TODO: Skip custom build if system gstreamer1.0-qt6 plugin is found # if(GST_PLUGIN_qml6_FOUND) @@ -41,56 +16,119 @@ endif() # GStreamer Good Plugins Integration # ============================================================================ +include(GStreamerHelpers) + +message(STATUS "gstqml6gl: GStreamer_VERSION = '${GStreamer_VERSION}'") + if(GStreamer_VERSION VERSION_GREATER_EQUAL "1.22") - # Use Latest Revisions for each minor version: 1.20.7, 1.22.12, 1.24.13, 1.26.6 + # Get recommended stable version for this GStreamer release string(REPLACE "." ";" GST_VERSION_LIST "${GStreamer_VERSION}") list(GET GST_VERSION_LIST 0 GST_VERSION_MAJOR) list(GET GST_VERSION_LIST 1 GST_VERSION_MINOR) - list(GET GST_VERSION_LIST 2 GST_VERSION_PATCH) - - if(GST_VERSION_MINOR EQUAL 20) - set(GST_VERSION_PATCH 7) - elseif(GST_VERSION_MINOR EQUAL 22) - set(GST_VERSION_PATCH 12) - elseif(GST_VERSION_MINOR EQUAL 24) - set(GST_VERSION_PATCH 13) - elseif(GST_VERSION_MINOR EQUAL 26) - set(GST_VERSION_PATCH 6) + set(GST_VERSION_PATCH "") + if(GStreamer_VERSION MATCHES "^${GST_VERSION_MAJOR}\\.${GST_VERSION_MINOR}\\.([0-9]+)") + set(GST_VERSION_PATCH "${CMAKE_MATCH_1}") endif() - set(GST_PLUGINS_VERSION ${GST_VERSION_MAJOR}.${GST_VERSION_MINOR}.${GST_VERSION_PATCH}) + # Use centralized version mapping from GStreamerHelpers.cmake + gstreamer_get_recommended_version( + ${GST_VERSION_MAJOR} + ${GST_VERSION_MINOR} + GST_PLUGINS_VERSION + "${GST_VERSION_PATCH}" + ) + + # Download gst-plugins-good sources for ext/qt6. + # Primary: standalone tarball from freedesktop.org (~4MB) + # Fallback: path-filtered GitLab archive of just ext/qt6 (~few KB) + gstreamer_get_package_url(good_plugins ${GST_PLUGINS_VERSION} _gst_good_url) + gstreamer_get_package_url(good_plugins_qt6 ${GST_PLUGINS_VERSION} _gst_qt6_url) + gstreamer_fetch_checksum(good_plugins ${GST_PLUGINS_VERSION} _gst_good_hash) + _gstreamer_is_release_like_config(_gst_release_like) + + set(_gst_qt6_cache "${CMAKE_BINARY_DIR}/_deps/gstreamer-good-plugins") + gstreamer_resilient_download( + URLS "${_gst_good_url}" + FILENAME "gst-plugins-good-${GST_PLUGINS_VERSION}.tar.xz" + DESTINATION_DIR "${_gst_qt6_cache}" + RESULT_VAR _gst_good_archive + EXPECTED_HASH "${_gst_good_hash}" + ALLOW_FAILURE + ) + + if(NOT _gst_good_archive) + if(_gst_release_like) + message(FATAL_ERROR + "gstqml6gl: Primary gst-plugins-good source download failed.\n" + "Release-like builds do not allow the unverified GitLab qt6 fallback archive.") + endif() + message(STATUS "gstqml6gl: Primary source tarball unavailable, trying GitLab qt6 fallback archive") + gstreamer_resilient_download( + URLS "${_gst_qt6_url}" + FILENAME "gstreamer-${GST_PLUGINS_VERSION}-qt6.tar.gz" + DESTINATION_DIR "${_gst_qt6_cache}" + RESULT_VAR _gst_good_archive + ) + endif() + + if(NOT _gst_good_archive) + message(FATAL_ERROR "gstqml6gl: Could not download gst-plugins-good sources for Qt6 plugin") + endif() - # Download GStreamer good plugins for Qt6 support CPMAddPackage( NAME gstreamer_good_plugins VERSION ${GST_PLUGINS_VERSION} - URL https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-${GST_PLUGINS_VERSION}.tar.xz - # TODO: Add URL_HASH for verification - # URL_HASH https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-${GST_PLUGINS_VERSION}.tar.xz.sha256sum + URL "file://${_gst_good_archive}" + DOWNLOAD_ONLY YES ) - set(QGC_GST_QT6_PLUGIN_PATH "${gstreamer_good_plugins_SOURCE_DIR}/ext/qt6") + + # Standalone tarball: ext/qt6 at root + # GitLab path-filtered archive: subprojects/gst-plugins-good/ext/qt6 + if(gstreamer_good_plugins_SOURCE_DIR) + if(EXISTS "${gstreamer_good_plugins_SOURCE_DIR}/ext/qt6") + set(QGC_GST_QT6_PLUGIN_PATH "${gstreamer_good_plugins_SOURCE_DIR}/ext/qt6") + elseif(EXISTS "${gstreamer_good_plugins_SOURCE_DIR}/subprojects/gst-plugins-good/ext/qt6") + set(QGC_GST_QT6_PLUGIN_PATH "${gstreamer_good_plugins_SOURCE_DIR}/subprojects/gst-plugins-good/ext/qt6") + endif() + endif() + + if(NOT QGC_GST_QT6_PLUGIN_PATH) + message(FATAL_ERROR "Failed to locate ext/qt6 in downloaded gst-plugins-good sources") + endif() else() + message(STATUS "gstqml6gl: GStreamer < 1.22, using local Qt6 fallback sources") set(QGC_GST_QT6_PLUGIN_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qt6") endif() +message(STATUS "gstqml6gl: QGC_GST_QT6_PLUGIN_PATH = ${QGC_GST_QT6_PLUGIN_PATH}") + +# Copy to build tree so we don't modify CPM's download cache +set(_qt6_build_sources "${CMAKE_CURRENT_BINARY_DIR}/qt6_sources") +file(REMOVE_RECURSE "${_qt6_build_sources}") +file(COPY "${QGC_GST_QT6_PLUGIN_PATH}/" DESTINATION "${_qt6_build_sources}") +set(QGC_GST_QT6_PLUGIN_PATH "${_qt6_build_sources}") + # ============================================================================ # Source Code Patching # ============================================================================ -# Fix missing includes in GStreamer Qt6 plugin -file(READ "${QGC_GST_QT6_PLUGIN_PATH}/qt6glitem.h" FILE_CONTENTS) -string(FIND "${FILE_CONTENTS}" "#include " GST_FIX_INCLUDES) -if(GST_FIX_INCLUDES EQUAL -1) - string(REPLACE "#include " "#include \n#include " FILE_CONTENTS "${FILE_CONTENTS}") - file(WRITE "${QGC_GST_QT6_PLUGIN_PATH}/qt6glitem.h" "${FILE_CONTENTS}") +# Upstream qt6glitem.h can miss QQuickWindow include, which breaks moc-generated +# metatype code with newer Qt. Patch the copied source tree only. +set(_qt6glitem_header "${QGC_GST_QT6_PLUGIN_PATH}/qt6glitem.h") +if(EXISTS "${_qt6glitem_header}") + file(READ "${_qt6glitem_header}" FILE_CONTENTS) + string(FIND "${FILE_CONTENTS}" "#include " GST_FIX_INCLUDES) + if(GST_FIX_INCLUDES EQUAL -1) + string(REPLACE + "#include " + "#include \n#include " + FILE_CONTENTS + "${FILE_CONTENTS}" + ) + file(WRITE "${_qt6glitem_header}" "${FILE_CONTENTS}") + endif() endif() -# ---------------------------------------------------------------------------- -# Build GStreamer QML6 GL Library -# ---------------------------------------------------------------------------- - -# NOTE: Using file(GLOB) for external GStreamer plugin sources -# CONFIGURE_DEPENDS ensures CMake re-runs if files are added/removed file(GLOB gstqml6gl_SRCS CONFIGURE_DEPENDS "${QGC_GST_QT6_PLUGIN_PATH}/*.cc" @@ -124,25 +162,23 @@ target_precompile_headers(gstqml6gl # Shader Compilation # ============================================================================ -if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/resources.qrc") - find_package(Qt6 REQUIRED COMPONENTS ShaderTools) - - set(SHADERS - "${QGC_GST_QT6_PLUGIN_PATH}/vertex.vert" - "${QGC_GST_QT6_PLUGIN_PATH}/YUV_TRIPLANAR.frag" - "${QGC_GST_QT6_PLUGIN_PATH}/RGBA.frag" - ) +# Discover shader files dynamically to support different GStreamer versions +file(GLOB _shader_files + "${QGC_GST_QT6_PLUGIN_PATH}/*.vert" + "${QGC_GST_QT6_PLUGIN_PATH}/*.frag" +) +# GLES-specific shaders are handled separately via qsb-wrapper below +list(FILTER _shader_files EXCLUDE REGEX "_gles\\.frag$") - set(OUTPUTS - vertex.vert.qsb - YUV_TRIPLANAR.frag.qsb - RGBA.frag.qsb - ) +if(_shader_files) + find_package(Qt6 REQUIRED COMPONENTS ShaderTools) - if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/YUV_BIPLANAR.frag") - LIST(APPEND SHADERS "${QGC_GST_QT6_PLUGIN_PATH}/YUV_BIPLANAR.frag") - LIST(APPEND OUTPUTS "YUV_BIPLANAR.frag.qsb") - endif() + set(SHADERS ${_shader_files}) + set(OUTPUTS) + foreach(_shader IN LISTS SHADERS) + get_filename_component(_name "${_shader}" NAME) + list(APPEND OUTPUTS "${_name}.qsb") + endforeach() # Compile shaders for different GLSL versions qt_add_shaders(gstqml6gl "gstqml6gl_shaders" @@ -155,7 +191,10 @@ if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/resources.qrc") ) # Handle additional GLES-specific RGBA shader if present + # This generates RGBA.frag.qsb.external which adds GLES 100 support to the base shader if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/RGBA_gles.frag") + find_package(Python3 REQUIRED COMPONENTS Interpreter) + find_program(QSB_PROGRAM NAMES qsb HINTS ${QT_HOST_PATH} ${QT_ROOT_DIR} ${QTDIR} @@ -164,19 +203,23 @@ if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/resources.qrc") REQUIRED ) - find_program(QSB_WRAPPER - NAMES qsb-wrapper.py - PATHS "${QGC_GST_QT6_PLUGIN_PATH}" - REQUIRED - ) + set(QSB_WRAPPER "${QGC_GST_QT6_PLUGIN_PATH}/qsb-wrapper.py") + if(NOT EXISTS "${QSB_WRAPPER}") + message(FATAL_ERROR "qsb-wrapper.py not found at ${QSB_WRAPPER}") + endif() + # The base shader is generated by qt_add_shaders into the .qsb directory set(RGBA_BASE_QSB "${CMAKE_CURRENT_BINARY_DIR}/.qsb/RGBA.frag.qsb") set(RGBA_GLES_SRC "${QGC_GST_QT6_PLUGIN_PATH}/RGBA_gles.frag") set(RGBA_EXTERNAL "${CMAKE_CURRENT_BINARY_DIR}/.qsb/RGBA.frag.qsb.external") + # Ensure the .qsb directory exists + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.qsb") + add_custom_command( OUTPUT ${RGBA_EXTERNAL} - COMMAND ${QSB_WRAPPER} + COMMAND ${Python3_EXECUTABLE} + ${QSB_WRAPPER} ${QSB_PROGRAM} ${RGBA_EXTERNAL} ${RGBA_GLES_SRC} @@ -184,15 +227,26 @@ if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/resources.qrc") DEPENDS ${RGBA_BASE_QSB} ${RGBA_GLES_SRC} + ${QSB_WRAPPER} COMMENT "Generating external GLES shader: ${RGBA_EXTERNAL}" VERBATIM ) + # Create a target for the external shader to ensure proper dependency ordering + # The custom command depends on RGBA_BASE_QSB file, which is generated by gstqml6gl_shaders target + add_custom_target(gstqml6gl_external_shader DEPENDS ${RGBA_EXTERNAL}) + if(gstqml6gl_shaders) + add_dependencies(gstqml6gl_external_shader ${gstqml6gl_shaders}) + endif() + set_source_files_properties("${RGBA_EXTERNAL}" PROPERTIES QT_RESOURCE_ALIAS RGBA.frag.qsb.external) qt_add_resources(gstqml6gl "gstqml6gl_shaders1" PREFIX "/org/freedesktop/gstreamer/qml6" FILES "${RGBA_EXTERNAL}" ) + + # Ensure the resource target depends on the external shader being generated + add_dependencies(gstqml6gl gstqml6gl_external_shader) endif() endif() @@ -212,8 +266,8 @@ if(GStreamer_GlWayland_FOUND) find_package(Qt6 COMPONENTS WaylandClient) if(TARGET Qt6::WaylandClient) target_link_libraries(gstqml6gl PRIVATE Qt6::WaylandClient) + target_compile_definitions(gstqml6gl PRIVATE HAVE_QT_WAYLAND) endif() - target_compile_definitions(gstqml6gl PRIVATE HAVE_QT_WAYLAND) endif() if(ANDROID) diff --git a/tools/setup/build-gstreamer.py b/tools/setup/build-gstreamer.py index bfec09dcea6c..0332e70d47ba 100755 --- a/tools/setup/build-gstreamer.py +++ b/tools/setup/build-gstreamer.py @@ -605,8 +605,8 @@ def main() -> int: # Resolve defaults version = args.version or get_build_config_value( - 'gstreamer_version', - '1.24.10', + 'gstreamer_default_version', + '1.24.13', start=Path(__file__).resolve(), ) arch = args.arch or get_default_arch(args.platform) diff --git a/tools/setup/resolve_gstreamer_config.py b/tools/setup/resolve_gstreamer_config.py index ef4dd4a8ad03..6f71367976ef 100644 --- a/tools/setup/resolve_gstreamer_config.py +++ b/tools/setup/resolve_gstreamer_config.py @@ -24,6 +24,7 @@ "macos": "gstreamer_macos_version", "windows": "gstreamer_windows_version", "android": "gstreamer_android_version", + "ios": "gstreamer_ios_version", } @@ -31,7 +32,7 @@ def resolve_version(platform_name: str, requested_version: str) -> str: """Return explicit version override or platform-specific config version.""" if requested_version: return requested_version - version_key = VERSION_KEYS.get(platform_name, "gstreamer_version") + version_key = VERSION_KEYS.get(platform_name, "gstreamer_default_version") return get_build_config_value(version_key, start=Path(__file__).resolve()) From ec20b4859f615f8b31813b1ac0057b96f6f37019 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:52:30 +0000 Subject: [PATCH 067/119] ci(deps): bump the github-actions group across 1 directory with 4 updates Bumps the github-actions group with 4 updates in the / directory: [actions/upload-artifact](https://github.com/actions/upload-artifact), [actions/download-artifact](https://github.com/actions/download-artifact), [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) and [actions/attest-sbom](https://github.com/actions/attest-sbom). Updates `actions/upload-artifact` from 6 to 7 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) Updates `actions/download-artifact` from 7 to 8 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) Updates `aquasecurity/trivy-action` from 0.34.0 to 0.34.1 - [Release notes](https://github.com/aquasecurity/trivy-action/releases) - [Commits](https://github.com/aquasecurity/trivy-action/compare/0.34.0...0.34.1) Updates `actions/attest-sbom` from 3 to 4 - [Release notes](https://github.com/actions/attest-sbom/releases) - [Changelog](https://github.com/actions/attest-sbom/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-sbom/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: aquasecurity/trivy-action dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: actions/attest-sbom dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/analysis.yml | 2 +- .github/workflows/android.yml | 2 +- .github/workflows/build-results.yml | 2 +- .github/workflows/check-links.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/docs_deploy.yml | 4 ++-- .github/workflows/doxygen_deploy.yml | 4 ++-- .github/workflows/flatpak.yml | 2 +- .github/workflows/linux.yml | 8 ++++---- .github/workflows/performance.yml | 4 ++-- .github/workflows/pre-commit.yml | 2 +- .github/workflows/release.yml | 10 +++++----- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 5125e926d9e1..10b17b2ae139 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -218,7 +218,7 @@ jobs: - name: Upload Output if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ inputs.tool }}-output path: | diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index c9154b454ab3..cae1dbc45aa3 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -364,7 +364,7 @@ jobs: - name: Upload Emulator Diagnostics if: ${{ failure() && matrix.emulator }} - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: emulator-diagnostics-${{ matrix.host }}-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/emulator-diagnostics diff --git a/.github/workflows/build-results.yml b/.github/workflows/build-results.yml index 02af08e9830b..dfc5c9b07c04 100644 --- a/.github/workflows/build-results.yml +++ b/.github/workflows/build-results.yml @@ -122,7 +122,7 @@ jobs: - name: Download pre-commit results artifact if: steps.pr.outputs.result != 'null' && steps.builds.outputs.all_complete == 'true' && steps.builds.outputs.precommit_run_id != '' continue-on-error: true - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: run-id: ${{ steps.builds.outputs.precommit_run_id }} github-token: ${{ github.token }} diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index b7e4a0f1b664..3e759c532925 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -66,7 +66,7 @@ jobs: fail: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - name: Upload report - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() with: name: link-check-report diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 183f346614e7..cd95a562254a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -170,7 +170,7 @@ jobs: - name: Scan artifact for vulnerabilities if: steps.artifact.outputs.found == 'true' - uses: aquasecurity/trivy-action@0.34.0 + uses: aquasecurity/trivy-action@0.35.0 with: scan-type: 'fs' scan-ref: ${{ steps.artifact.outputs.path }} diff --git a/.github/workflows/docs_deploy.yml b/.github/workflows/docs_deploy.yml index 9e10da04256e..31cf10caacd9 100644 --- a/.github/workflows/docs_deploy.yml +++ b/.github/workflows/docs_deploy.yml @@ -48,7 +48,7 @@ jobs: touch docs/.vitepress/dist/.nojekyll - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: qgc_docs_build path: docs/.vitepress/dist/ @@ -67,7 +67,7 @@ jobs: egress-policy: audit - name: Download Artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: qgc_docs_build path: ~/_book diff --git a/.github/workflows/doxygen_deploy.yml b/.github/workflows/doxygen_deploy.yml index df4dcd03c57d..e375bbc50d6a 100644 --- a/.github/workflows/doxygen_deploy.yml +++ b/.github/workflows/doxygen_deploy.yml @@ -37,7 +37,7 @@ jobs: run: doxygen Doxyfile - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: doxygen_build path: docs/api/html/ @@ -56,7 +56,7 @@ jobs: egress-policy: audit - name: Download Artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: doxygen_build path: ~/_api_docs diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index c8ec0f4c963d..14f5512c3e6b 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -34,7 +34,7 @@ jobs: cache-key: flatpak-builder-${{ hashFiles('deploy/linux/org.mavlink.qgroundcontrol.flatpak.yml') }} - name: Upload Flatpak - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: QGroundControl-flatpak path: QGroundControl.flatpak diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index d71e866f04fd..a56ba18c624c 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -177,7 +177,7 @@ jobs: - name: Upload metrics artifact if: matrix.build_type == 'Release' && matrix.os == 'ubuntu-22.04' - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: size-metrics path: ${{ runner.temp }}/build/metrics.json @@ -371,7 +371,7 @@ jobs: - name: Upload Duration Report if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: test-duration-linux_gcc_64 path: ${{ runner.temp }}/build/test-duration-linux-linux_gcc_64.json @@ -379,7 +379,7 @@ jobs: - name: Upload Test Results (Sanitizers) if: always() && !cancelled() && matrix.mode == 'sanitizers' - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: test-results-linux-sanitizers path: | @@ -496,7 +496,7 @@ jobs: egress-policy: audit - name: Download metrics artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: size-metrics path: . diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 3bccabbacc43..a204d7bcb416 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -65,7 +65,7 @@ jobs: run: python3 ./.github/scripts/find_binary.py --build-dir build --build-type Release - name: Upload binary - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: qgc-binary path: | @@ -114,7 +114,7 @@ jobs: modules: ${{ steps.config.outputs.qt_modules }} - name: Download binary - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: qgc-binary path: build diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index e12126d719b6..d9951b3f4500 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -89,7 +89,7 @@ jobs: - name: Upload pre-commit results artifact if: github.event_name == 'pull_request' && always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: pre-commit-results path: pre-commit-results diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec00a84d237c..b54f1ee43e25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -126,7 +126,7 @@ jobs: artifact-name: sbom-cyclonedx - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: path: artifacts merge-multiple: true @@ -135,28 +135,28 @@ jobs: run: find artifacts -type f | head -50 || echo "No artifacts found" - name: Attest Linux AppImage - uses: actions/attest-sbom@v3 + uses: actions/attest-sbom@v4 with: subject-path: artifacts/**/QGroundControl*.AppImage sbom-path: sbom.spdx.json continue-on-error: true - name: Attest macOS DMG - uses: actions/attest-sbom@v3 + uses: actions/attest-sbom@v4 with: subject-path: artifacts/**/QGroundControl.dmg sbom-path: sbom.spdx.json continue-on-error: true - name: Attest Windows EXE - uses: actions/attest-sbom@v3 + uses: actions/attest-sbom@v4 with: subject-path: artifacts/**/QGroundControl*.exe sbom-path: sbom.spdx.json continue-on-error: true - name: Attest Android APK - uses: actions/attest-sbom@v3 + uses: actions/attest-sbom@v4 with: subject-path: artifacts/**/QGroundControl.apk sbom-path: sbom.spdx.json From 311c038ac36ad08f8b9b5ac862d1d644d8b15812 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 11 Mar 2026 13:36:17 -0700 Subject: [PATCH 068/119] Rename home position label from 'Launch' to 'Home' --- src/FlightMap/Widgets/CenterMapDropButton.qml | 2 +- src/FlightMap/Widgets/CenterMapDropPanel.qml | 2 +- src/MissionManager/MissionSettingsItem.cc | 2 +- src/PlanView/TakeoffItemMapVisual.qml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/FlightMap/Widgets/CenterMapDropButton.qml b/src/FlightMap/Widgets/CenterMapDropButton.qml index d8fd38f8f8f2..eb3ad652a0c0 100644 --- a/src/FlightMap/Widgets/CenterMapDropButton.qml +++ b/src/FlightMap/Widgets/CenterMapDropButton.qml @@ -175,7 +175,7 @@ DropButton { } QGCButton { - text: qsTr("Launch") + text: qsTr("Home") Layout.fillWidth: true enabled: !followVehicleCheckBox.checked diff --git a/src/FlightMap/Widgets/CenterMapDropPanel.qml b/src/FlightMap/Widgets/CenterMapDropPanel.qml index fe5528f3975f..372ce85c4ee2 100644 --- a/src/FlightMap/Widgets/CenterMapDropPanel.qml +++ b/src/FlightMap/Widgets/CenterMapDropPanel.qml @@ -40,7 +40,7 @@ ColumnLayout { } QGCButton { - text: qsTr("Launch") + text: qsTr("Home") Layout.fillWidth: true onClicked: { diff --git a/src/MissionManager/MissionSettingsItem.cc b/src/MissionManager/MissionSettingsItem.cc index 1586c6c5bfac..02ba471867db 100644 --- a/src/MissionManager/MissionSettingsItem.cc +++ b/src/MissionManager/MissionSettingsItem.cc @@ -234,7 +234,7 @@ void MissionSettingsItem::_setHomeAltFromTerrain(double terrainAltitude) QString MissionSettingsItem::abbreviation(void) const { - return _flyView ? tr("L") : tr("Launch"); + return _flyView ? tr("H") : tr("Home"); } void MissionSettingsItem::_updateFlyViewHomePosition(const QGeoCoordinate& homePosition) diff --git a/src/PlanView/TakeoffItemMapVisual.qml b/src/PlanView/TakeoffItemMapVisual.qml index f175d3121004..20985a3758a7 100644 --- a/src/PlanView/TakeoffItemMapVisual.qml +++ b/src/PlanView/TakeoffItemMapVisual.qml @@ -118,7 +118,7 @@ Item { sourceItem: MissionItemIndexLabel { checked: _missionItem.isCurrentItem - label: qsTr("Launch") + label: qsTr("Home") highlightSelected: true onClicked: _root.clicked(_missionItem.sequenceNumber) visible: _root.interactive From 12a9cb91bc23f2fd2a5621a9970f2fa6286394c0 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 11 Mar 2026 13:36:41 -0700 Subject: [PATCH 069/119] Rename AltitudeMode to AltitudeFrame throughout codebase Systematically rename the AltMode enum and all related types, properties, methods, signals, and QML components to use AltitudeFrame terminology, which better reflects that these represent MAVLink altitude reference frames rather than abstract modes. Key changes: - Enum: AltMode -> AltitudeFrame, values like AltModeMixed -> AltitudeFrameMixed - C++ properties/signals: altitudeMode -> altitudeFrame throughout - Methods: altitudeModeExtraUnits -> altitudeFrameExtraUnits, etc. - QML files: AltModeCombo -> AltFrameCombo, AltModeDialog -> AltFrameDialog - Pass currentAltFrame to AltFrameDialog open calls - Fix _missionItem -> missionItem in TransectStyleComplexItemTerrainFollow - Fix _amslAltAboveTerrainFact loading from wrong JSON key - Remove unused SettingsFixture::setAltitudeFrame no-op - JSON keys preserved for backward compatibility --- docs/en/qgc-user-guide/plan_view/plan_view.md | 2 +- .../FactControls/AltitudeFactTextField.qml | 12 +- src/MissionManager/CameraCalc.cc | 20 ++-- src/MissionManager/CameraCalc.h | 10 +- src/MissionManager/MissionController.cc | 68 +++++------ src/MissionManager/MissionController.h | 16 +-- src/MissionManager/SimpleMissionItem.cc | 104 ++++++++--------- src/MissionManager/SimpleMissionItem.h | 16 +-- src/MissionManager/SurveyComplexItem.cc | 2 +- src/MissionManager/TakeoffMissionItem.cc | 2 +- .../TransectStyleComplexItem.cc | 110 +++++++++--------- src/PlanView/CameraCalcGrid.qml | 4 +- src/PlanView/FWLandingPatternEditor.qml | 6 +- src/PlanView/MissionDefaultsEditor.qml | 36 +++--- src/PlanView/SimpleItemEditor.qml | 22 ++-- src/PlanView/StructureScanEditor.qml | 4 +- src/PlanView/SurveyItemEditor.qml | 4 +- .../TransectStyleComplexItemTerrainFollow.qml | 22 ++-- src/PlanView/VTOLLandingPatternEditor.qml | 6 +- src/QmlControls/AltFrameCombo.qml | 60 ++++++++++ .../{AltModeDialog.qml => AltFrameDialog.qml} | 59 ++++------ src/QmlControls/AltModeCombo.qml | 68 ----------- src/QmlControls/CMakeLists.txt | 4 +- src/QmlControls/QGroundControlQmlGlobal.cc | 54 +++++---- src/QmlControls/QGroundControlQmlGlobal.h | 22 ++-- test/MissionManager/CameraCalcTest.cc | 6 +- test/MissionManager/MissionControllerTest.cc | 26 ++--- test/MissionManager/MissionControllerTest.h | 2 +- test/MissionManager/SimpleMissionItemTest.cc | 32 ++--- .../Fixtures/RAIIFixtures.cc | 5 - .../UnitTestFramework/Fixtures/RAIIFixtures.h | 3 - 31 files changed, 388 insertions(+), 419 deletions(-) create mode 100644 src/QmlControls/AltFrameCombo.qml rename src/QmlControls/{AltModeDialog.qml => AltFrameDialog.qml} (52%) delete mode 100644 src/QmlControls/AltModeCombo.qml diff --git a/docs/en/qgc-user-guide/plan_view/plan_view.md b/docs/en/qgc-user-guide/plan_view/plan_view.md index 5c32aae14b74..06db598c7dcb 100644 --- a/docs/en/qgc-user-guide/plan_view/plan_view.md +++ b/docs/en/qgc-user-guide/plan_view/plan_view.md @@ -121,7 +121,7 @@ The Plan Info section contains general plan-level settings: The Defaults section sets plan-wide values that apply to new mission items: -- **Altitude Mode** — Select the altitude reference frame for waypoints. +- **Altitude Frame** — Select the altitude reference frame for waypoints. - **Waypoints Altitude** — The default altitude for the first mission item added (subsequent items take their initial altitude from the previous item). Changing this value when items already exist will prompt to update all items to the new altitude. - **Flight Speed** — Set a flight speed that differs from the default mission speed. - **Vehicle Speeds** — Cruise speed (fixed-wing) and/or hover speed (multi-rotor/VTOL), used for estimating mission time. diff --git a/src/FactSystem/FactControls/AltitudeFactTextField.qml b/src/FactSystem/FactControls/AltitudeFactTextField.qml index b6796650919f..8015855aceae 100644 --- a/src/FactSystem/FactControls/AltitudeFactTextField.qml +++ b/src/FactSystem/FactControls/AltitudeFactTextField.qml @@ -7,17 +7,17 @@ import QGroundControl.Controls FactTextField { unitsLabel: fact ? fact.units : "" - extraUnitsLabel: fact ? _altitudeModeExtraUnits : "" + extraUnitsLabel: fact ? qsTr("%1").arg(_altitudeFrameExtraUnits) : "" showUnits: true showHelp: true - property int altitudeMode: QGroundControl.AltitudeModeNone + property int altitudeFrame: QGroundControl.AltitudeFrameNone - property string _altitudeModeExtraUnits + property string _altitudeFrameExtraUnits - onAltitudeModeChanged: updateAltitudeModeExtraUnits() + onAltitudeFrameChanged: updateAltitudeFrameExtraUnits() - function updateAltitudeModeExtraUnits() { - _altitudeModeExtraUnits = QGroundControl.altitudeModeExtraUnits(altitudeMode); + function updateAltitudeFrameExtraUnits() { + _altitudeFrameExtraUnits = QGroundControl.altitudeFrameExtraUnits(altitudeFrame); } } diff --git a/src/MissionManager/CameraCalc.cc b/src/MissionManager/CameraCalc.cc index 17fe3c72bf57..78522a0b238f 100644 --- a/src/MissionManager/CameraCalc.cc +++ b/src/MissionManager/CameraCalc.cc @@ -8,7 +8,7 @@ CameraCalc::CameraCalc(PlanMasterController* masterController, const QString& settingsGroup, QObject* parent) : CameraSpec (settingsGroup, parent) - , _distanceMode (masterController->missionController()->globalAltitudeModeDefault()) + , _distanceMode (masterController->missionController()->globalAltitudeFrameDefault()) , _knownCameraList (masterController->controllerVehicle()->staticCameraList()) , _metaDataMap (FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/CameraCalc.FactMetaData.json"), this)) , _cameraNameFact (settingsGroup, _metaDataMap[cameraNameName]) @@ -110,9 +110,9 @@ void CameraCalc::_cameraNameChanged(void) } _recalcTriggerDistance(); - if (!isManualCamera() && distanceMode() == QGroundControlQmlGlobal::AltitudeModeAbsolute) { + if (!isManualCamera() && distanceMode() == QGroundControlQmlGlobal::AltitudeFrameAbsolute) { // Manual grids support absolute alts whereas nothing else does. Make sure we are not left in absolute - setDistanceMode(QGroundControlQmlGlobal::AltitudeModeRelative); + setDistanceMode(QGroundControlQmlGlobal::AltitudeFrameRelative); } } @@ -203,11 +203,11 @@ bool CameraCalc::load(const QJsonObject& originalJson, bool deprecatedFollowTerr if (version == 1) { // Version 1->2 differences: // - _jsonDistanceToSurfaceRelativeKeyDeprecated changed to distanceMode - // - deprecatedFollowTerrain value was loaded from upper level callers and represents AltitudeModeCalcAboveTerrain. AtitudeModeTerrainFrame was not supported yet. + // - deprecatedFollowTerrain value was loaded from upper level callers and represents AltitudeFrameCalcAboveTerrain. AltitudeFrameTerrain was not supported yet. if (deprecatedFollowTerrain) { - json[distanceModeName] = QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain; + json[distanceModeName] = QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain; } else { - json[distanceModeName] = json[_jsonDistanceToSurfaceRelativeKeyDeprecated].toBool() ? QGroundControlQmlGlobal::AltitudeModeRelative : QGroundControlQmlGlobal::AltitudeModeAbsolute; + json[distanceModeName] = json[_jsonDistanceToSurfaceRelativeKeyDeprecated].toBool() ? QGroundControlQmlGlobal::AltitudeFrameRelative : QGroundControlQmlGlobal::AltitudeFrameAbsolute; } json.remove(_jsonDistanceToSurfaceRelativeKeyDeprecated); version = 2; @@ -235,7 +235,7 @@ bool CameraCalc::load(const QJsonObject& originalJson, bool deprecatedFollowTerr QString canonicalCameraName = _validCanonicalCameraName(json[cameraNameName].toString()); _cameraNameFact.setRawValue(canonicalCameraName); - setDistanceMode(static_cast(json[distanceModeName].toInt())); + setDistanceMode(static_cast(json[distanceModeName].toInt())); _adjustedFootprintSideFact.setRawValue (json[adjustedFootprintSideName].toDouble()); _adjustedFootprintFrontalFact.setRawValue (json[adjustedFootprintFrontalName].toDouble()); @@ -293,10 +293,10 @@ QString CameraCalc::xlatManualCameraName(void) return tr("Manual (no camera specs)"); } -void CameraCalc::setDistanceMode(QGroundControlQmlGlobal::AltMode altMode) +void CameraCalc::setDistanceMode(QGroundControlQmlGlobal::AltitudeFrame altFrame) { - if (altMode != _distanceMode) { - _distanceMode = altMode; + if (altFrame != _distanceMode) { + _distanceMode = altFrame; emit distanceModeChanged(_distanceMode); } } diff --git a/src/MissionManager/CameraCalc.h b/src/MissionManager/CameraCalc.h index 2308e3177f13..054297ceef4c 100644 --- a/src/MissionManager/CameraCalc.h +++ b/src/MissionManager/CameraCalc.h @@ -38,7 +38,7 @@ class CameraCalc : public CameraSpec // grid altitude mode - distanceMode // trigger distance - adjustedFootprintFrontal // transect spacing - adjustedFootprintSide - Q_PROPERTY(QGroundControlQmlGlobal::AltMode distanceMode READ distanceMode WRITE setDistanceMode NOTIFY distanceModeChanged) + Q_PROPERTY(QGroundControlQmlGlobal::AltitudeFrame distanceMode READ distanceMode WRITE setDistanceMode NOTIFY distanceModeChanged) // The following values are calculated from the camera properties Q_PROPERTY(double imageFootprintSide READ imageFootprintSide NOTIFY imageFootprintSideChanged) ///< Size of image size side in meters @@ -69,9 +69,9 @@ class CameraCalc : public CameraSpec bool isCustomCamera (void) const { return _cameraNameFact.rawValue().toString() == canonicalCustomCameraName(); } double imageFootprintSide (void) const { return _imageFootprintSide; } double imageFootprintFrontal (void) const { return _imageFootprintFrontal; } - QGroundControlQmlGlobal::AltMode distanceMode(void) const { return _distanceMode; } + QGroundControlQmlGlobal::AltitudeFrame distanceMode(void) const { return _distanceMode; } - void setDistanceMode (QGroundControlQmlGlobal::AltMode altMode); + void setDistanceMode (QGroundControlQmlGlobal::AltitudeFrame altFrame); void setCameraBrand (const QString& cameraBrand); void setCameraModel (const QString& cameraModel); @@ -93,7 +93,7 @@ class CameraCalc : public CameraSpec signals: void imageFootprintSideChanged (double imageFootprintSide); void imageFootprintFrontalChanged (double imageFootprintFrontal); - void distanceModeChanged (int altMode); + void distanceModeChanged (int altFrame); void isManualCameraChanged (void); void isCustomCameraChanged (void); void cameraBrandChanged (void); @@ -116,7 +116,7 @@ private slots: QString _cameraModel; QStringList _cameraBrandList; QStringList _cameraModelList; - QGroundControlQmlGlobal::AltMode _distanceMode = QGroundControlQmlGlobal::AltitudeModeRelative; + QGroundControlQmlGlobal::AltitudeFrame _distanceMode = QGroundControlQmlGlobal::AltitudeFrameRelative; double _imageFootprintSide = 0; double _imageFootprintFrontal = 0; QVariantList _knownCameraList; diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index ea0bb2a467b6..1f372bf3b21c 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -194,8 +194,8 @@ void MissionController::_newMissionItemsAvailableFromVehicle(bool removeAllReque _visualItems = newControllerMissionItems; _settingsItem = settingsItem; - // We set Altitude mode to mixed, otherwise if we need a non relative altitude frame we won't be able to change it - setGlobalAltitudeMode(weHaveItemsFromVehicle ? QGroundControlQmlGlobal::AltitudeModeMixed : QGroundControlQmlGlobal::AltitudeModeRelative); + // We set Altitude frame to mixed, otherwise if we need a non relative altitude frame we won't be able to change it + setGlobalAltitudeFrame(weHaveItemsFromVehicle ? QGroundControlQmlGlobal::AltitudeFrameMixed : QGroundControlQmlGlobal::AltitudeFrameRelative); MissionController::_scanForAdditionalSettings(_visualItems, _masterController); @@ -315,13 +315,13 @@ VisualMissionItem* MissionController::_insertSimpleMissionItemWorker(QGeoCoordin if (newItem->specifiesAltitude()) { if (!MissionCommandTree::instance()->isLandCommand(command)) { double prevAltitude; - QGroundControlQmlGlobal::AltMode prevAltMode; + QGroundControlQmlGlobal::AltitudeFrame prevAltFrame; - if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltMode)) { + if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) { newItem->altitude()->setRawValue(prevAltitude); - if (globalAltitudeMode() == QGroundControlQmlGlobal::AltitudeModeMixed) { - // We are in mixed altitude modes, so copy from previous. Otherwise alt mode will be set from global setting. - newItem->setAltitudeMode(static_cast(prevAltMode)); + if (globalAltitudeFrame() == QGroundControlQmlGlobal::AltitudeFrameMixed) { + // We are in mixed altitude frames, so copy from previous. Otherwise altitude frame will be set from global setting. + newItem->setAltitudeFrame(static_cast(prevAltFrame)); } } } @@ -359,11 +359,11 @@ VisualMissionItem* MissionController::insertTakeoffItem(QGeoCoordinate /*coordin if (_takeoffMissionItem->specifiesAltitude()) { double prevAltitude; - QGroundControlQmlGlobal::AltMode prevAltMode; + QGroundControlQmlGlobal::AltitudeFrame prevAltFrame; - if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltMode)) { + if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) { _takeoffMissionItem->altitude()->setRawValue(prevAltitude); - _takeoffMissionItem->setAltitudeMode(static_cast(prevAltMode)); + _takeoffMissionItem->setAltitudeFrame(static_cast(prevAltFrame)); } } if (visualItemIndex == -1) { @@ -437,11 +437,11 @@ VisualMissionItem* MissionController::insertComplexMissionItem(QString itemName, newItem->setCoordinate(mapCenterCoordinate); double prevAltitude; - QGroundControlQmlGlobal::AltMode prevAltMode; - if (globalAltitudeMode() == QGroundControlQmlGlobal::AltitudeModeMixed) { - // We are in mixed altitude modes, so copy from previous. Otherwise alt mode will be set from global setting in constructor. - if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltMode)) { - qobject_cast(newItem)->cameraCalc()->setDistanceMode(prevAltMode); + QGroundControlQmlGlobal::AltitudeFrame prevAltFrame; + if (globalAltitudeFrame() == QGroundControlQmlGlobal::AltitudeFrameMixed) { + // We are in mixed altitude frames, so copy from previous. Otherwise alt mode will be set from global setting in constructor. + if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) { + qobject_cast(newItem)->cameraCalc()->setDistanceMode(prevAltFrame); } } } else if (itemName == FixedWingLandingComplexItem::name) { @@ -636,7 +636,7 @@ bool MissionController::_loadJsonMissionFileV1(const QJsonObject& json, QmlObjec return false; } - setGlobalAltitudeMode(QGroundControlQmlGlobal::AltitudeModeMixed); + setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameMixed); // Read complex items QList surveyItems; @@ -741,7 +741,7 @@ bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjec return false; } - setGlobalAltitudeMode(QGroundControlQmlGlobal::AltitudeModeMixed); + setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameMixed); qCDebug(MissionControllerLog) << "MissionController::_loadJsonMissionFileV2 itemCount:" << json[_jsonItemsKey].toArray().count(); @@ -774,7 +774,7 @@ bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjec appSettings->offlineEditingHoverSpeed()->setRawValue(json[_jsonHoverSpeedKey].toDouble()); } if (json.contains(_jsonGlobalPlanAltitudeModeKey)) { - setGlobalAltitudeMode(json[_jsonGlobalPlanAltitudeModeKey].toVariant().value()); + setGlobalAltitudeFrame(json[_jsonGlobalPlanAltitudeModeKey].toVariant().value()); } QGeoCoordinate homeCoordinate; @@ -1041,7 +1041,7 @@ bool MissionController::loadTextFile(QFile& file, QString& errorString) QByteArray bytes = file.readAll(); QTextStream stream(bytes); - setGlobalAltitudeMode(QGroundControlQmlGlobal::AltitudeModeMixed); + setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameMixed); QmlObjectListModel* loadedVisualItems = new QmlObjectListModel(this); if (!_loadTextMissionFile(stream, loadedVisualItems, errorStr)) { @@ -1084,7 +1084,7 @@ void MissionController::save(QJsonObject& json) json[_jsonVehicleTypeKey] = _controllerVehicle->vehicleType(); json[_jsonCruiseSpeedKey] = _controllerVehicle->defaultCruiseSpeed(); json[_jsonHoverSpeedKey] = _controllerVehicle->defaultHoverSpeed(); - json[_jsonGlobalPlanAltitudeModeKey] = _globalAltMode; + json[_jsonGlobalPlanAltitudeModeKey] = _globalAltFrame; // Save the visual items @@ -2193,11 +2193,11 @@ void MissionController::_inProgressChanged(bool inProgress) emit syncInProgressChanged(inProgress); } -bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltMode* prevAltitudeMode) +bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltitudeFrame* prevAltitudeMode) { bool found = false; double foundAltitude = 0; - QGroundControlQmlGlobal::AltMode foundAltMode = QGroundControlQmlGlobal::AltitudeModeNone; + QGroundControlQmlGlobal::AltitudeFrame foundAltFrame = QGroundControlQmlGlobal::AltitudeFrameNone; if (newIndex > _visualItems->count()) { return false; @@ -2212,7 +2212,7 @@ bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude SimpleMissionItem* simpleItem = qobject_cast(visualItem); if (simpleItem->specifiesAltitude()) { foundAltitude = simpleItem->altitude()->rawValue().toDouble(); - foundAltMode = simpleItem->altitudeMode(); + foundAltFrame = simpleItem->altitudeFrame(); found = true; break; } @@ -2222,7 +2222,7 @@ bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude if (found) { *prevAltitude = foundAltitude; - *prevAltitudeMode = foundAltMode; + *prevAltitudeMode = foundAltFrame; } return found; @@ -2971,24 +2971,24 @@ MissionController::SendToVehiclePreCheckState MissionController::sendToVehiclePr return SendToVehiclePreCheckStateOk; } -QGroundControlQmlGlobal::AltMode MissionController::globalAltitudeMode(void) +QGroundControlQmlGlobal::AltitudeFrame MissionController::globalAltitudeFrame(void) { - return _globalAltMode; + return _globalAltFrame; } -QGroundControlQmlGlobal::AltMode MissionController::globalAltitudeModeDefault(void) +QGroundControlQmlGlobal::AltitudeFrame MissionController::globalAltitudeFrameDefault(void) { - if (_globalAltMode == QGroundControlQmlGlobal::AltitudeModeMixed) { - return QGroundControlQmlGlobal::AltitudeModeRelative; + if (_globalAltFrame == QGroundControlQmlGlobal::AltitudeFrameMixed) { + return QGroundControlQmlGlobal::AltitudeFrameRelative; } else { - return _globalAltMode; + return _globalAltFrame; } } -void MissionController::setGlobalAltitudeMode(QGroundControlQmlGlobal::AltMode altMode) +void MissionController::setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altFrame) { - if (_globalAltMode != altMode) { - _globalAltMode = altMode; - emit globalAltitudeModeChanged(); + if (_globalAltFrame != altFrame) { + _globalAltFrame = altFrame; + emit globalAltitudeFrameChanged(); } } diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index a4811e51fa3f..e14f8e6ef26b 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -120,8 +120,8 @@ class MissionController : public PlanElementController Q_PROPERTY(double minAMSLAltitude MEMBER _minAMSLAltitude NOTIFY minAMSLAltitudeChanged) ///< Minimum altitude associated with this mission. Used to calculate percentages for terrain status. Q_PROPERTY(double maxAMSLAltitude MEMBER _maxAMSLAltitude NOTIFY maxAMSLAltitudeChanged) ///< Maximum altitude associated with this mission. Used to calculate percentages for terrain status. - Q_PROPERTY(QGroundControlQmlGlobal::AltMode globalAltitudeMode READ globalAltitudeMode WRITE setGlobalAltitudeMode NOTIFY globalAltitudeModeChanged) - Q_PROPERTY(QGroundControlQmlGlobal::AltMode globalAltitudeModeDefault READ globalAltitudeModeDefault NOTIFY globalAltitudeModeChanged) ///< Default to use for newly created items + Q_PROPERTY(QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrame READ globalAltitudeFrame WRITE setGlobalAltitudeFrame NOTIFY globalAltitudeFrameChanged) + Q_PROPERTY(QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrameDefault READ globalAltitudeFrameDefault NOTIFY globalAltitudeFrameChanged) ///< Default to use for newly created items Q_INVOKABLE void removeVisualItem(int viIndex); @@ -317,9 +317,9 @@ class MissionController : public PlanElementController bool isFirstLandingComplexItem (const LandingComplexItem* item) const; bool isEmpty (void) const; - QGroundControlQmlGlobal::AltMode globalAltitudeMode(void); - QGroundControlQmlGlobal::AltMode globalAltitudeModeDefault(void); - void setGlobalAltitudeMode(QGroundControlQmlGlobal::AltMode altMode); + QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrame(void); + QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrameDefault(void); + void setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altFrame); // Top-level group row indices in _visualItemsTree (must match _setupTreeModel order) static constexpr int kPlanFileGroupRow = 0; @@ -371,7 +371,7 @@ class MissionController : public PlanElementController void recalcTerrainProfile (void); void _recalcMissionFlightStatusSignal (void); void _recalcFlightPathSegmentsSignal (void); - void globalAltitudeModeChanged (void); + void globalAltitudeFrameChanged (void); private slots: void _newMissionItemsAvailableFromVehicle (bool removeAllRequested); @@ -410,7 +410,7 @@ private slots: void _deinitVisualItem (VisualMissionItem* item); void _setupActiveVehicle (Vehicle* activeVehicle, bool forceLoadFromVehicle); void _calcPrevWaypointValues (VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference); - bool _findPreviousAltitude (int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltMode* prevAltMode); + bool _findPreviousAltitude (int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltitudeFrame* prevAltFrame); MissionSettingsItem* _addMissionSettings (QmlObjectListModel* visualItems); bool _loadJsonMissionFile (const QByteArray& bytes, QmlObjectListModel* visualItems, QString& errorString); bool _loadJsonMissionFileV1 (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); @@ -496,7 +496,7 @@ private slots: double _maxAMSLAltitude = 0; bool _missionContainsVTOLTakeoff = false; - QGroundControlQmlGlobal::AltMode _globalAltMode = QGroundControlQmlGlobal::AltitudeModeRelative; + QGroundControlQmlGlobal::AltitudeFrame _globalAltFrame = QGroundControlQmlGlobal::AltitudeFrameRelative; static constexpr const char* _settingsGroup = "MissionController"; static constexpr const char* _jsonFileTypeValue = "Mission"; diff --git a/src/MissionManager/SimpleMissionItem.cc b/src/MissionManager/SimpleMissionItem.cc index 8f886c6d52bf..5014d8d8fe59 100644 --- a/src/MissionManager/SimpleMissionItem.cc +++ b/src/MissionManager/SimpleMissionItem.cc @@ -66,21 +66,21 @@ SimpleMissionItem::SimpleMissionItem(PlanMasterController* masterController, boo { _editorQml = QStringLiteral("qrc:/qml/QGroundControl/PlanView/SimpleItemEditor.qml"); - struct MavFrame2AltMode_s { + struct MavFrame2AltFrame_s { MAV_FRAME mavFrame; - QGroundControlQmlGlobal::AltMode altMode; + QGroundControlQmlGlobal::AltitudeFrame altFrame; }; - const struct MavFrame2AltMode_s rgMavFrame2AltMode[] = { - { MAV_FRAME_GLOBAL_TERRAIN_ALT, QGroundControlQmlGlobal::AltitudeModeTerrainFrame }, - { MAV_FRAME_GLOBAL, QGroundControlQmlGlobal::AltitudeModeAbsolute }, - { MAV_FRAME_GLOBAL_RELATIVE_ALT, QGroundControlQmlGlobal::AltitudeModeRelative }, + const struct MavFrame2AltFrame_s rgMavFrame2AltFrame[] = { + { MAV_FRAME_GLOBAL_TERRAIN_ALT, QGroundControlQmlGlobal::AltitudeFrameTerrain }, + { MAV_FRAME_GLOBAL, QGroundControlQmlGlobal::AltitudeFrameAbsolute }, + { MAV_FRAME_GLOBAL_RELATIVE_ALT, QGroundControlQmlGlobal::AltitudeFrameRelative }, }; - _altitudeMode = QGroundControlQmlGlobal::AltitudeModeRelative; - for (size_t i=0; iappSettings()->defaultMissionItemAltitude()->rawValue().toDouble(); _altitudeFact.setRawValue(defaultAlt); _missionItem._param7Fact.setRawValue(defaultAlt); - // Note that setAltitudeMode will also set MAV_FRAME correctly through signalling + // Note that setAltitudeFrame will also set MAV_FRAME correctly through signalling // Takeoff items always use relative alt since that is the highest quality data to base altitude from - setAltitudeMode(isTakeoffItem() ? QGroundControlQmlGlobal::AltitudeModeRelative : _missionController->globalAltitudeModeDefault()); + setAltitudeFrame(isTakeoffItem() ? QGroundControlQmlGlobal::AltitudeFrameRelative : _missionController->globalAltitudeFrameDefault()); } else { _altitudeFact.setRawValue(0); _missionItem._param7Fact.setRawValue(0); @@ -1058,11 +1058,11 @@ void SimpleMissionItem::setMissionFlightStatus(MissionController::MissionFlightS } } -void SimpleMissionItem::setAltitudeMode(QGroundControlQmlGlobal::AltMode altitudeMode) +void SimpleMissionItem::setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altitudeFrame) { - if (altitudeMode != _altitudeMode) { - _altitudeMode = altitudeMode; - emit altitudeModeChanged(); + if (altitudeFrame != _altitudeFrame) { + _altitudeFrame = altitudeFrame; + emit altitudeFrameChanged(); } } @@ -1112,23 +1112,23 @@ double SimpleMissionItem::editableAlt() const double SimpleMissionItem::amslEntryAlt(void) const { - switch (_altitudeMode) { - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + switch (_altitudeFrame) { + case QGroundControlQmlGlobal::AltitudeFrameTerrain: return _missionItem.param7() + _terrainAltitude; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: return _missionItem.param7(); - case QGroundControlQmlGlobal::AltitudeModeRelative: + case QGroundControlQmlGlobal::AltitudeFrameRelative: return _missionItem.param7() + _masterController->missionController()->plannedHomePosition().altitude(); - case QGroundControlQmlGlobal::AltitudeModeNone: - qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeMode:AltitudeModeNone"; + case QGroundControlQmlGlobal::AltitudeFrameNone: + qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeFrame:AltitudeFrameNone"; return qQNaN(); - case QGroundControlQmlGlobal::AltitudeModeMixed: - qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeMode:AltitudeModeMixed"; + case QGroundControlQmlGlobal::AltitudeFrameMixed: + qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeFrame:AltitudeFrameMixed"; return qQNaN(); } - qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeMode:" << _altitudeMode; + qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeFrame:" << _altitudeFrame; return qQNaN(); } diff --git a/src/MissionManager/SimpleMissionItem.h b/src/MissionManager/SimpleMissionItem.h index fa859674d668..3e275d622b14 100644 --- a/src/MissionManager/SimpleMissionItem.h +++ b/src/MissionManager/SimpleMissionItem.h @@ -24,9 +24,9 @@ class SimpleMissionItem : public VisualMissionItem Q_PROPERTY(bool friendlyEditAllowed READ friendlyEditAllowed NOTIFY friendlyEditAllowedChanged) Q_PROPERTY(bool rawEdit READ rawEdit WRITE setRawEdit NOTIFY rawEditChanged) ///< true: raw item editing with all params Q_PROPERTY(bool specifiesAltitude READ specifiesAltitude NOTIFY commandChanged) - Q_PROPERTY(Fact* altitude READ altitude CONSTANT) ///< Altitude as specified by altitudeMode. Not necessarily true mission item altitude - Q_PROPERTY(QGroundControlQmlGlobal::AltMode altitudeMode READ altitudeMode WRITE setAltitudeMode NOTIFY altitudeModeChanged) - Q_PROPERTY(Fact* amslAltAboveTerrain READ amslAltAboveTerrain CONSTANT) ///< Actual AMSL altitude for item if altitudeMode == AltitudeAboveTerrain + Q_PROPERTY(Fact* altitude READ altitude CONSTANT) ///< Altitude as specified by altitudeFrame. Not necessarily true mission item altitude + Q_PROPERTY(QGroundControlQmlGlobal::AltitudeFrame altitudeFrame READ altitudeFrame WRITE setAltitudeFrame NOTIFY altitudeFrameChanged) + Q_PROPERTY(Fact* amslAltAboveTerrain READ amslAltAboveTerrain CONSTANT) ///< Actual AMSL altitude for item if altitudeFrame is AltitudeFrameCalcAboveTerrain or AltitudeFrameTerrain Q_PROPERTY(int command READ command WRITE setCommand NOTIFY commandChanged) Q_PROPERTY(bool isLoiterItem READ isLoiterItem NOTIFY isLoiterItemChanged) Q_PROPERTY(bool showLoiterRadius READ showLoiterRadius NOTIFY showLoiterRadiusChanged) @@ -64,7 +64,7 @@ class SimpleMissionItem : public VisualMissionItem bool friendlyEditAllowed (void) const; bool rawEdit (void) const; bool specifiesAltitude (void) const; - QGroundControlQmlGlobal::AltMode altitudeMode(void) const { return _altitudeMode; } + QGroundControlQmlGlobal::AltitudeFrame altitudeFrame(void) const { return _altitudeFrame; } Fact* altitude (void) { return &_altitudeFact; } Fact* amslAltAboveTerrain (void) { return &_amslAltAboveTerrainFact; } bool isLoiterItem (void) const; @@ -82,7 +82,7 @@ class SimpleMissionItem : public VisualMissionItem QmlObjectListModel* comboboxFactsAdvanced (void) { return &_comboboxFactsAdvanced; } void setRawEdit(bool rawEdit); - void setAltitudeMode(QGroundControlQmlGlobal::AltMode altitudeMode); + void setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altitudeFrame); void setCommandByIndex(int index); @@ -142,7 +142,7 @@ class SimpleMissionItem : public VisualMissionItem void rawEditChanged (bool rawEdit); void cameraSectionChanged (QObject* cameraSection); void speedSectionChanged (QObject* cameraSection); - void altitudeModeChanged (void); + void altitudeFrameChanged (void); void isLoiterItemChanged (void); void showLoiterRadiusChanged (void); void loiterRadiusChanged (double loiterRadius); @@ -154,7 +154,7 @@ private slots: void _sendCoordinateChanged (void); void _sendFriendlyEditAllowedChanged (void); void _altitudeChanged (void); - void _altitudeModeChanged (void); + void _altitudeFrameChanged (void); void _terrainAltChanged (void); void _updateLastSequenceNumber (void); void _rebuildFacts (void); @@ -184,7 +184,7 @@ private slots: Fact _supportedCommandFact; - QGroundControlQmlGlobal::AltMode _altitudeMode = QGroundControlQmlGlobal::AltitudeModeRelative; + QGroundControlQmlGlobal::AltitudeFrame _altitudeFrame = QGroundControlQmlGlobal::AltitudeFrameRelative; Fact _altitudeFact; Fact _amslAltAboveTerrainFact; diff --git a/src/MissionManager/SurveyComplexItem.cc b/src/MissionManager/SurveyComplexItem.cc index 3868c378befe..2ccd192ed7fb 100644 --- a/src/MissionManager/SurveyComplexItem.cc +++ b/src/MissionManager/SurveyComplexItem.cc @@ -239,7 +239,7 @@ bool SurveyComplexItem::_loadV3(const QJsonObject& complexObject, int sequenceNu _cameraTriggerInTurnAroundFact.setRawValue (complexObject[_jsonV3CameraTriggerInTurnaroundKey].toBool(true)); _cameraCalc.valueSetIsDistance()->setRawValue (complexObject[_jsonV3FixedValueIsAltitudeKey].toBool(true)); - _cameraCalc.setDistanceMode(complexObject[_jsonV3GridAltitudeRelativeKey].toBool(true) ? QGroundControlQmlGlobal::AltitudeModeRelative : QGroundControlQmlGlobal::AltitudeModeAbsolute); + _cameraCalc.setDistanceMode(complexObject[_jsonV3GridAltitudeRelativeKey].toBool(true) ? QGroundControlQmlGlobal::AltitudeFrameRelative : QGroundControlQmlGlobal::AltitudeFrameAbsolute); bool manualGrid = complexObject[_jsonV3ManualGridKey].toBool(true); diff --git a/src/MissionManager/TakeoffMissionItem.cc b/src/MissionManager/TakeoffMissionItem.cc index 19248ddad33a..1536a6fa6b52 100644 --- a/src/MissionManager/TakeoffMissionItem.cc +++ b/src/MissionManager/TakeoffMissionItem.cc @@ -167,7 +167,7 @@ void TakeoffMissionItem::_setLaunchCoordinate(const QGeoCoordinate& launchCoordi if (_controllerVehicle->fixedWing()) { double altitude = this->altitude()->rawValue().toDouble(); - if (altitudeMode() == QGroundControlQmlGlobal::AltitudeModeRelative) { + if (altitudeFrame() == QGroundControlQmlGlobal::AltitudeFrameRelative) { // Offset for fixed wing climb out of 30 degrees to specified altitude if (altitude != 0.0) { distance = altitude / tan(qDegreesToRadians(30.0)); diff --git a/src/MissionManager/TransectStyleComplexItem.cc b/src/MissionManager/TransectStyleComplexItem.cc index 23615ed80ce4..3cf5efec3c15 100644 --- a/src/MissionManager/TransectStyleComplexItem.cc +++ b/src/MissionManager/TransectStyleComplexItem.cc @@ -150,7 +150,7 @@ void TransectStyleComplexItem::_save(QJsonObject& complexObject) innerObject[refly90DegreesName] = _refly90DegreesFact.rawValue().toBool(); innerObject[_jsonCameraShotsKey] = _cameraShots; - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { innerObject[terrainAdjustToleranceName] = _terrainAdjustToleranceFact.rawValue().toDouble(); innerObject[terrainAdjustMaxClimbRateName] = _terrainAdjustMaxClimbRateFact.rawValue().toDouble(); innerObject[terrainAdjustMaxDescentRateName] = _terrainAdjustMaxDescentRateFact.rawValue().toDouble(); @@ -284,7 +284,7 @@ bool TransectStyleComplexItem::_load(const QJsonObject& complexObject, bool forP _cameraShots = innerObject[_jsonCameraShotsKey].toInt(); } - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { QList followTerrainKeyInfoList = { { terrainAdjustToleranceName, QJsonValue::Double, true }, { terrainAdjustMaxClimbRateName, QJsonValue::Double, true }, @@ -317,7 +317,7 @@ bool TransectStyleComplexItem::_load(const QJsonObject& complexObject, bool forP } if (!forPresets) { - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeTerrainFrame) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameTerrain) { // Terrain frame requires terrain data in order to know AMSL coordinate heights for each mission item _queryMissionItemCoordHeights(); } else { @@ -410,18 +410,18 @@ void TransectStyleComplexItem::_rebuildTransects(void) _minAMSLAltitude = _maxAMSLAltitude = qQNaN(); switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: _rebuildTransects - invalid _cameraCalc.distanceMode()" << _cameraCalc.distanceMode(); return; - case QGroundControlQmlGlobal::AltitudeModeRelative: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameRelative: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: // Terrain height not needed to calculate path, as TerrainFrame specifies a fixed altitude over terrain, doesn't need to know the actual terrain height // so vehicle is responsible for having or not this altitude calculation, so we can build the flight path right away. _buildFlightPathCoordInfoFromTransects(); break; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: // Query the terrain data. Once available flight path will be calculated, as on this mode QGC actually calculates the individual altitude for each waypoint // having into account terrain data. _queryTransectsPathHeightInfo(); @@ -499,12 +499,12 @@ void TransectStyleComplexItem::_updateFlightPathSegmentsDontCallDirectly(void) _flightPathSegments.clearAndDeleteContents(); switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: _updateFlightPathSegmentsDontCallDirectly - invalid _cameraCalc.distanceMode()" << _cameraCalc.distanceMode(); return; - case QGroundControlQmlGlobal::AltitudeModeRelative: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameRelative: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: { // Since we aren't following terrain all the transects are at the same height. We can use _visualTransectPoints to build the // flight path segments. @@ -519,9 +519,9 @@ void TransectStyleComplexItem::_updateFlightPathSegmentsDontCallDirectly(void) } } break; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain && _loadedMissionItems.count()) { + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain && _loadedMissionItems.count()) { // Build segments from loaded mission item data QGeoCoordinate prevCoord = QGeoCoordinate(); double prevAlt = 0; @@ -540,7 +540,7 @@ void TransectStyleComplexItem::_updateFlightPathSegmentsDontCallDirectly(void) // - Working from loaded mission items which have had terrain heights queried for // In both cases _rgFlightPathCoordInfo will be set up for use if (_rgFlightPathCoordInfo.count()) { - FlightPathSegment::SegmentType segmentType = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain ? FlightPathSegment::SegmentTypeGeneric : FlightPathSegment::SegmentTypeTerrainFrame; + FlightPathSegment::SegmentType segmentType = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain ? FlightPathSegment::SegmentTypeGeneric : FlightPathSegment::SegmentTypeTerrainFrame; for (int i=0; i<_rgFlightPathCoordInfo.count() - 1; i++) { const QGeoCoordinate& fromCoord = _rgFlightPathCoordInfo[i].coord; const QGeoCoordinate& toCoord = _rgFlightPathCoordInfo[i+1].coord; @@ -667,7 +667,7 @@ void TransectStyleComplexItem::_missionItemCoordTerrainData(bool success, QList< TransectStyleComplexItem::ReadyForSaveState TransectStyleComplexItem::readyForSaveState(void) const { bool terrainReady = false; - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { if (_loadedMissionItems.count()) { // We have loaded mission items. Everything is ready to go. terrainReady = true; @@ -688,15 +688,15 @@ TransectStyleComplexItem::ReadyForSaveState TransectStyleComplexItem::readyForSa void TransectStyleComplexItem::_adjustForAvailableTerrainData(void) { switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: _adjustForAvailableTerrainData - invalid _cameraCalc.distanceMode()" << _cameraCalc.distanceMode(); return; - case QGroundControlQmlGlobal::AltitudeModeRelative: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameRelative: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: // No additional work needed return; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: _buildFlightPathCoordInfoFromPathHeightInfoForCalcAboveTerrain(); _adjustForMaxRates(); _adjustForTolerance(); @@ -708,7 +708,7 @@ void TransectStyleComplexItem::_adjustForAvailableTerrainData(void) } emit lastSequenceNumberChanged(lastSequenceNumber()); break; - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: if (_loadedMissionItems.count()) { _buildFlightPathCoordInfoFromMissionItems(); } else { @@ -1105,7 +1105,7 @@ int TransectStyleComplexItem::lastSequenceNumber(void) const void TransectStyleComplexItem::_distanceModeChanged(int distanceMode) { - if (static_cast(distanceMode) == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (static_cast(distanceMode) == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { _refly90DegreesFact.setRawValue(false); _hoverAndCaptureFact.setRawValue(false); } @@ -1131,7 +1131,7 @@ void TransectStyleComplexItem::appendMissionItems(QList& items, QO void TransectStyleComplexItem::_appendWaypoint(QList& items, QObject* missionItemParent, int& seqNum, MAV_FRAME mavFrame, float holdTime, const QGeoCoordinate& coordinate) { - double altitude = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain ? coordinate.altitude() : _cameraCalc.distanceToSurface()->rawValue().toDouble(); + double altitude = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain ? coordinate.altitude() : _cameraCalc.distanceToSurface()->rawValue().toDouble(); MissionItem* item = new MissionItem(seqNum++, MAV_CMD_NAV_WAYPOINT, @@ -1167,7 +1167,7 @@ void TransectStyleComplexItem::_appendSinglePhotoCapture(QList& it void TransectStyleComplexItem::_appendConditionGate(QList& items, QObject* missionItemParent, int& seqNum, MAV_FRAME mavFrame, const QGeoCoordinate& coordinate) { - double altitude = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain ? coordinate.altitude() : _cameraCalc.distanceToSurface()->rawValue().toDouble(); + double altitude = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain ? coordinate.altitude() : _cameraCalc.distanceToSurface()->rawValue().toDouble(); MissionItem* item = new MissionItem(seqNum++, MAV_CMD_CONDITION_GATE, @@ -1232,18 +1232,18 @@ void TransectStyleComplexItem::_buildAndAppendMissionItems(QList& qCDebug(TransectStyleComplexItemLog) << "_buildAndAppendMissionItems"; switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeRelative: + case QGroundControlQmlGlobal::AltitudeFrameRelative: mavFrame = MAV_FRAME_GLOBAL_RELATIVE_ALT; break; - case QGroundControlQmlGlobal::AltitudeModeAbsolute: - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: mavFrame = MAV_FRAME_GLOBAL; break; - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: mavFrame = MAV_FRAME_GLOBAL_TERRAIN_ALT; break; - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: _buildAndAppendMissionItems incorrect _cameraCalc.distanceMode" << _cameraCalc.distanceMode(); mavFrame = MAV_FRAME_GLOBAL_RELATIVE_ALT; break; @@ -1352,25 +1352,25 @@ double TransectStyleComplexItem::amslEntryAlt(void) const double distanceToSurface = _cameraCalc.distanceToSurface()->rawValue().toDouble(); switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeRelative: + case QGroundControlQmlGlobal::AltitudeFrameRelative: alt = distanceToSurface + _missionController->plannedHomePosition().altitude(); break; - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: alt = distanceToSurface; break; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: if (_loadedMissionItems.count()) { // The first item might not be a waypoint we have to find it. for (int i=0; i<_loadedMissionItems.count(); i++) { MissionItem* item = _loadedMissionItems[i]; const MissionCommandUIInfo* uiInfo = MissionCommandTree::instance()->getUIInfo(_controllerVehicle, QGCMAVLink::VehicleClassGeneric, item->command()); if (uiInfo && uiInfo->specifiesCoordinate() && !uiInfo->isStandaloneCoordinate()) { - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { - // AltitudeModeCalcAboveTerrain has AMSL alt in param 7 + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { + // AltitudeFrameCalcAboveTerrain has AMSL alt in param 7 alt = item->param7(); } else { - // AltitudeModeTerrainFrame has terrain frame relative alt in param 7. So we need terrain heights to calc AMSL. + // AltitudeFrameTerrain has terrain frame relative alt in param 7. So we need terrain heights to calc AMSL. if (_rgPathHeightInfo.count()) { alt = item->param7() + _rgPathHeightInfo.first().heights.first(); } @@ -1384,8 +1384,8 @@ double TransectStyleComplexItem::amslEntryAlt(void) const } } break; - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: amslEntryAlt incorrect _cameraCalc.distanceMode" << _cameraCalc.distanceMode(); break; } @@ -1398,23 +1398,23 @@ double TransectStyleComplexItem::amslExitAlt(void) const double alt = qQNaN(); switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeRelative: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameRelative: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: alt = amslEntryAlt(); break; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: if (_loadedMissionItems.count()) { // The last item might not be a waypoint we have to find it. for (int i=_loadedMissionItems.count()-1; i>0; i--) { MissionItem* item = _loadedMissionItems[i]; const MissionCommandUIInfo* uiInfo = MissionCommandTree::instance()->getUIInfo(_controllerVehicle, QGCMAVLink::VehicleClassGeneric, item->command()); if (uiInfo && uiInfo->specifiesCoordinate() && !uiInfo->isStandaloneCoordinate()) { - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { - // AltitudeModeCalcAboveTerrain has AMSL alt in param 7 + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { + // AltitudeFrameCalcAboveTerrain has AMSL alt in param 7 alt = item->param7(); } else { - // AltitudeModeTerrainFrame has terrain frame relative alt in param 7. So we need terrain heights to calc AMSL. + // AltitudeFrameTerrain has terrain frame relative alt in param 7. So we need terrain heights to calc AMSL. if (_rgPathHeightInfo.count()) { alt = item->param7() + _rgPathHeightInfo.last().heights.last(); } @@ -1428,8 +1428,8 @@ double TransectStyleComplexItem::amslExitAlt(void) const } } break; - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: amslExitAlt incorrect _cameraCalc.distanceMode" << _cameraCalc.distanceMode(); break; } @@ -1447,10 +1447,10 @@ double TransectStyleComplexItem::minAMSLAltitude(void) const { // FIXME: What about terrain frame - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { return _minAMSLAltitude; } else { - return _cameraCalc.distanceToSurface()->rawValue().toDouble() + (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeRelative ? _missionController->plannedHomePosition().altitude() : 0); + return _cameraCalc.distanceToSurface()->rawValue().toDouble() + (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameRelative ? _missionController->plannedHomePosition().altitude() : 0); } } @@ -1458,9 +1458,9 @@ double TransectStyleComplexItem::maxAMSLAltitude(void) const { // FIXME: What about terrain frame - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { return _maxAMSLAltitude; } else { - return _cameraCalc.distanceToSurface()->rawValue().toDouble() + (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeRelative ? _missionController->plannedHomePosition().altitude() : 0); + return _cameraCalc.distanceToSurface()->rawValue().toDouble() + (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameRelative ? _missionController->plannedHomePosition().altitude() : 0); } } diff --git a/src/PlanView/CameraCalcGrid.qml b/src/PlanView/CameraCalcGrid.qml index 50c0c6fe5fe6..7e081d517c2c 100644 --- a/src/PlanView/CameraCalcGrid.qml +++ b/src/PlanView/CameraCalcGrid.qml @@ -87,7 +87,7 @@ Column { AltitudeFactTextField { fact: cameraCalc.distanceToSurface - altitudeMode: cameraCalc.distanceMode + altitudeFrame: cameraCalc.distanceMode enabled: fixedDistanceRadio.checked Layout.fillWidth: true } @@ -120,7 +120,7 @@ Column { QGCLabel { text: distanceToSurfaceLabel } AltitudeFactTextField { fact: cameraCalc.distanceToSurface - altitudeMode: cameraCalc.distanceMode + altitudeFrame: cameraCalc.distanceMode Layout.fillWidth: true } diff --git a/src/PlanView/FWLandingPatternEditor.qml b/src/PlanView/FWLandingPatternEditor.qml index 15974fb44b0f..5c2b7bcfdde3 100644 --- a/src/PlanView/FWLandingPatternEditor.qml +++ b/src/PlanView/FWLandingPatternEditor.qml @@ -26,7 +26,7 @@ Rectangle { property real _spacer: ScreenTools.defaultFontPixelWidth / 2 property string _setToVehicleHeadingStr: qsTr("Set to vehicle heading") property string _setToVehicleLocationStr: qsTr("Set to vehicle location") - property int _altitudeMode: missionItem.altitudesAreRelative ? QGroundControl.AltitudeModeRelative : QGroundControl.AltitudeModeAbsolute + property int _altitudeFrame: missionItem.altitudesAreRelative ? QGroundControl.AltitudeFrameRelative : QGroundControl.AltitudeFrameAbsolute Column { @@ -67,7 +67,7 @@ Rectangle { AltitudeFactTextField { Layout.fillWidth: true fact: missionItem.finalApproachAltitude - altitudeMode: _altitudeMode + altitudeFrame: _altitudeFrame } FactCheckBox { @@ -141,7 +141,7 @@ Rectangle { AltitudeFactTextField { Layout.fillWidth: true fact: missionItem.landingAltitude - altitudeMode: _altitudeMode + altitudeFrame: _altitudeFrame } QGCRadioButton { diff --git a/src/PlanView/MissionDefaultsEditor.qml b/src/PlanView/MissionDefaultsEditor.qml index 5166c6b2c50f..e8dd6d2a2556 100644 --- a/src/PlanView/MissionDefaultsEditor.qml +++ b/src/PlanView/MissionDefaultsEditor.qml @@ -31,17 +31,17 @@ Rectangle { target: _root._controllerVehicle function onFirmwareTypeChanged() { if (!_root._controllerVehicle.supports.terrainFrame - && _root.missionController.globalAltitudeMode === QGroundControl.AltitudeModeTerrainFrame) { - _root.missionController.globalAltitudeMode = QGroundControl.AltitudeModeCalcAboveTerrain + && _root.missionController.globalAltitudeFrame === QGroundControl.AltitudeFrameTerrain) { + _root.missionController.globalAltitudeFrame = QGroundControl.AltitudeFrameCalcAboveTerrain } } } - Component { id: altModeDialogComponent; AltModeDialog { } } + Component { id: altFrameDialogComponent; AltFrameDialog { } } QGCPopupDialogFactory { - id: altModeDialogFactory - dialogComponent: altModeDialogComponent + id: altFrameDialogFactory + dialogComponent: altFrameDialogComponent } ColumnLayout { @@ -54,30 +54,30 @@ Rectangle { LabelledButton { Layout.fillWidth: true - label: qsTr("Altitude Mode") - buttonText: QGroundControl.altitudeModeShortDescription(_root.missionController.globalAltitudeMode) + label: qsTr("Alt Frame") + buttonText: QGroundControl.altitudeFrameExtraUnits(_root.missionController.globalAltitudeFrame) onClicked: { let removeModes = [] - let updateFunction = function(altMode) { _root.missionController.globalAltitudeMode = altMode } + let updateFunction = function(altFrame) { _root.missionController.globalAltitudeFrame = altFrame } if (!_root._controllerVehicle.supports.terrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) + removeModes.push(QGroundControl.AltitudeFrameTerrain) } if (!_root._noMissionItemsAdded) { - if (_root.missionController.globalAltitudeMode !== QGroundControl.AltitudeModeRelative) { - removeModes.push(QGroundControl.AltitudeModeRelative) + if (_root.missionController.globalAltitudeFrame !== QGroundControl.AltitudeFrameRelative) { + removeModes.push(QGroundControl.AltitudeFrameRelative) } - if (_root.missionController.globalAltitudeMode !== QGroundControl.AltitudeModeAbsolute) { - removeModes.push(QGroundControl.AltitudeModeAbsolute) + if (_root.missionController.globalAltitudeFrame !== QGroundControl.AltitudeFrameAbsolute) { + removeModes.push(QGroundControl.AltitudeFrameAbsolute) } - if (_root.missionController.globalAltitudeMode !== QGroundControl.AltitudeModeCalcAboveTerrain) { - removeModes.push(QGroundControl.AltitudeModeCalcAboveTerrain) + if (_root.missionController.globalAltitudeFrame !== QGroundControl.AltitudeFrameCalcAboveTerrain) { + removeModes.push(QGroundControl.AltitudeFrameCalcAboveTerrain) } - if (_root.missionController.globalAltitudeMode !== QGroundControl.AltitudeModeTerrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) + if (_root.missionController.globalAltitudeFrame !== QGroundControl.AltitudeFrameTerrain) { + removeModes.push(QGroundControl.AltitudeFrameTerrain) } } - altModeDialogFactory.open({ rgRemoveModes: removeModes, updateAltModeFn: updateFunction }) + altFrameDialogFactory.open({ currentAltFrame: _root.missionController.globalAltitudeFrame, rgRemoveModes: removeModes, updateAltFrameFn: updateFunction }) } } diff --git a/src/PlanView/SimpleItemEditor.qml b/src/PlanView/SimpleItemEditor.qml index a6c2160d6051..c984d1b7223b 100644 --- a/src/PlanView/SimpleItemEditor.qml +++ b/src/PlanView/SimpleItemEditor.qml @@ -18,8 +18,8 @@ Rectangle { property real _margin: ScreenTools.defaultFontPixelHeight / 2 property real _altRectMargin: ScreenTools.defaultFontPixelWidth / 2 property var _controllerVehicle: missionItem.masterController.controllerVehicle - property int _globalAltMode: missionItem.masterController.missionController.globalAltitudeMode - property bool _globalAltModeIsMixed: _globalAltMode == QGroundControl.AltitudeModeMixed + property int _globalAltFrame: missionItem.masterController.missionController.globalAltitudeFrame + property bool _globalAltFrameIsMixed: _globalAltFrame == QGroundControl.AltitudeFrameMixed property real _radius: ScreenTools.defaultFontPixelWidth / 2 property real _fieldSpacing: ScreenTools.defaultFontPixelHeight / 2 @@ -144,17 +144,17 @@ Rectangle { RowLayout { Layout.fillWidth: true - visible: _globalAltModeIsMixed + visible: _globalAltFrameIsMixed QGCLabel { Layout.fillWidth: true - text: qsTr("Altitude Mode") + text: qsTr("Alt Frame") } - AltModeCombo { - altitudeMode: missionItem.altitudeMode + AltFrameCombo { + altitudeFrame: missionItem.altitudeFrame vehicle: _controllerVehicle - onAltitudeModeChanged: missionItem.altitudeMode = altitudeMode + onAltitudeFrameChanged: missionItem.altitudeFrame = altitudeFrame } } @@ -165,18 +165,14 @@ Rectangle { fact: missionItem.altitude function _extraLabelText() { - if (!_globalAltModeIsMixed && missionItem.altitudeMode !== QGroundControl.AltitudeModeRelative) { - return qsTr(" (%1)").arg(QGroundControl.altitudeModeShortDescription(missionItem.altitudeMode)) - } else { - return "" - } + return qsTr(" (%1)").arg(QGroundControl.altitudeFrameExtraUnits(missionItem.altitudeFrame)) } } QGCLabel { font.pointSize: ScreenTools.smallFontPointSize text: qsTr("Actual AMSL alt sent: %1 %2").arg(missionItem.amslAltAboveTerrain.valueString).arg(missionItem.amslAltAboveTerrain.units) - visible: missionItem.altitudeMode === QGroundControl.AltitudeModeCalcAboveTerrain + visible: missionItem.altitudeFrame === QGroundControl.AltitudeFrameCalcAboveTerrain } } diff --git a/src/PlanView/StructureScanEditor.qml b/src/PlanView/StructureScanEditor.qml index 94f624954800..4c183233400d 100644 --- a/src/PlanView/StructureScanEditor.qml +++ b/src/PlanView/StructureScanEditor.qml @@ -140,14 +140,14 @@ Rectangle { QGCLabel { text: qsTr("Scan Bottom Alt") } AltitudeFactTextField { fact: missionItem.scanBottomAlt - altitudeMode: QGroundControl.AltitudeModeRelative + altitudeFrame: QGroundControl.AltitudeFrameRelative Layout.fillWidth: true } QGCLabel { text: qsTr("Entrance/Exit Alt") } AltitudeFactTextField { fact: missionItem.entranceAlt - altitudeMode: QGroundControl.AltitudeModeRelative + altitudeFrame: QGroundControl.AltitudeFrameRelative Layout.fillWidth: true } diff --git a/src/PlanView/SurveyItemEditor.qml b/src/PlanView/SurveyItemEditor.qml index c99df42a8346..7db2dfbf6d34 100644 --- a/src/PlanView/SurveyItemEditor.qml +++ b/src/PlanView/SurveyItemEditor.qml @@ -70,13 +70,13 @@ TransectStyleComplexItemEditor { { text: qsTr("Hover and capture image"), fact: missionItem.hoverAndCapture, - enabled: missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeModeRelative || missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeModeAbsolute, + enabled: missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeFrameRelative || missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeFrameAbsolute, visible: missionItem.hoverAndCaptureAllowed }, { text: qsTr("Refly at 90 deg offset"), fact: missionItem.refly90Degrees, - enabled: missionItem.cameraCalc.distanceMode !== QGroundControl.AltitudeModeCalcAboveTerrain, + enabled: missionItem.cameraCalc.distanceMode !== QGroundControl.AltitudeFrameCalcAboveTerrain, visible: true }, { diff --git a/src/PlanView/TransectStyleComplexItemTerrainFollow.qml b/src/PlanView/TransectStyleComplexItemTerrainFollow.qml index de1872f6a353..1d289091628e 100644 --- a/src/PlanView/TransectStyleComplexItemTerrainFollow.qml +++ b/src/PlanView/TransectStyleComplexItemTerrainFollow.qml @@ -18,29 +18,29 @@ ColumnLayout { onClicked: { var removeModes = [] - var updateFunction = function(altMode){ missionItem.cameraCalc.distanceMode = altMode } - removeModes.push(QGroundControl.AltitudeModeMixed) + var updateFunction = function(altFrame){ missionItem.cameraCalc.distanceMode = altFrame } + removeModes.push(QGroundControl.AltitudeFrameMixed) if (!missionItem.masterController.controllerVehicle.supports.terrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) + removeModes.push(QGroundControl.AltitudeFrameTerrain) } - if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude || !_missionItem.cameraCalc.isManualCamera) { - removeModes.push(QGroundControl.AltitudeModeAbsolute) + if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude || !missionItem.cameraCalc.isManualCamera) { + removeModes.push(QGroundControl.AltitudeFrameAbsolute) } - altModeDialogFactory.open({ rgRemoveModes: removeModes, updateAltModeFn: updateFunction }) + altFrameDialogFactory.open({ currentAltFrame: missionItem.cameraCalc.distanceMode, rgRemoveModes: removeModes, updateAltFrameFn: updateFunction }) } QGCPopupDialogFactory { - id: altModeDialogFactory + id: altFrameDialogFactory - dialogComponent: altModeDialogComponent + dialogComponent: altFrameDialogComponent } - Component { id: altModeDialogComponent; AltModeDialog { } } + Component { id: altFrameDialogComponent; AltFrameDialog { } } RowLayout { spacing: ScreenTools.defaultFontPixelWidth / 2 - QGCLabel { text: QGroundControl.altitudeModeShortDescription(missionItem.cameraCalc.distanceMode) } + QGCLabel { text: QGroundControl.altitudeFrameShortDescription(missionItem.cameraCalc.distanceMode) } QGCColoredImage { height: ScreenTools.defaultFontPixelHeight / 2 width: height @@ -55,7 +55,7 @@ ColumnLayout { columnSpacing: _margin rowSpacing: _margin columns: 2 - enabled: missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeModeCalcAboveTerrain + enabled: missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeFrameCalcAboveTerrain QGCLabel { text: qsTr("Tolerance") } FactTextField { diff --git a/src/PlanView/VTOLLandingPatternEditor.qml b/src/PlanView/VTOLLandingPatternEditor.qml index 4a6402df208f..9f349efba09f 100644 --- a/src/PlanView/VTOLLandingPatternEditor.qml +++ b/src/PlanView/VTOLLandingPatternEditor.qml @@ -26,7 +26,7 @@ Rectangle { property real _spacer: ScreenTools.defaultFontPixelWidth / 2 property string _setToVehicleHeadingStr: qsTr("Set to vehicle heading") property string _setToVehicleLocationStr: qsTr("Set to vehicle location") - property int _altitudeMode: missionItem.altitudesAreRelative ? QGroundControl.AltitudeModeRelative : QGroundControl.AltitudeModeAbsolute + property int _altitudeFrame: missionItem.altitudesAreRelative ? QGroundControl.AltitudeFrameRelative : QGroundControl.AltitudeFrameAbsolute Column { @@ -67,7 +67,7 @@ Rectangle { AltitudeFactTextField { Layout.fillWidth: true fact: missionItem.finalApproachAltitude - altitudeMode: _altitudeMode + altitudeFrame: _altitudeFrame } QGCLabel { @@ -129,7 +129,7 @@ Rectangle { AltitudeFactTextField { Layout.fillWidth: true fact: missionItem.landingAltitude - altitudeMode: _altitudeMode + altitudeFrame: _altitudeFrame } QGCLabel { text: qsTr("Landing Dist") } diff --git a/src/QmlControls/AltFrameCombo.qml b/src/QmlControls/AltFrameCombo.qml new file mode 100644 index 000000000000..f0e3620c9d9b --- /dev/null +++ b/src/QmlControls/AltFrameCombo.qml @@ -0,0 +1,60 @@ +import QtQuick + +import QGroundControl +import QGroundControl.Controls + +QGCComboBox { + required property int altitudeFrame + required property var vehicle + + textRole: "modeName" + + onActivated: (index) => { + let modeValue = altFrameModel.get(index).modeValue + altitudeFrame = modeValue + } + + ListModel { + id: altFrameModel + } + + Component.onCompleted: { + altFrameModel.append({ modeName: QGroundControl.altitudeFrameExtraUnits(QGroundControl.AltitudeFrameRelative), + modeValue: QGroundControl.AltitudeFrameRelative }) + altFrameModel.append({ modeName: QGroundControl.altitudeFrameExtraUnits(QGroundControl.AltitudeFrameAbsolute), + modeValue: QGroundControl.AltitudeFrameAbsolute }) + altFrameModel.append({ modeName: QGroundControl.altitudeFrameExtraUnits(QGroundControl.AltitudeFrameTerrain), + modeValue: QGroundControl.AltitudeFrameTerrain }) + altFrameModel.append({ modeName: QGroundControl.altitudeFrameExtraUnits(QGroundControl.AltitudeFrameCalcAboveTerrain), + modeValue: QGroundControl.AltitudeFrameCalcAboveTerrain }) + + let removeModes = [] + + if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude && altitudeFrame != QGroundControl.AltitudeFrameAbsolute) { + removeModes.push(QGroundControl.AltitudeFrameAbsolute) + } + if (!vehicle.supports.terrainFrame) { + removeModes.push(QGroundControl.AltitudeFrameTerrain) + } + + // Remove modes specified by consumer + for (var i=0; i { - let modeValue = altModeModel.get(index).modeValue - altitudeMode = modeValue - } - - ListModel { - id: altModeModel - - ListElement { - modeName: qsTr("Relative") - modeValue: QGroundControl.AltitudeModeRelative - } - ListElement { - modeName: qsTr("Absolute") - modeValue: QGroundControl.AltitudeModeAbsolute - } - ListElement { - modeName: qsTr("Terrain") - modeValue: QGroundControl.AltitudeModeTerrainFrame - } - ListElement { - modeName: qsTr("TerrainC") - modeValue: QGroundControl.AltitudeModeCalcAboveTerrain - } - } - - Component.onCompleted: { - let removeModes = [] - - if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude && altitudeMode != QGroundControl.AltitudeModeAbsolute) { - removeModes.push(QGroundControl.AltitudeModeAbsolute) - } - if (!vehicle.supports.terrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) - } - - // Remove modes specified by consumer - for (var i=0; iclearAllSignals(); } rgFacts.clear(); - _cameraCalc->setDistanceMode(_cameraCalc->distanceMode() == QGroundControlQmlGlobal::AltitudeModeRelative - ? QGroundControlQmlGlobal::AltitudeModeAbsolute - : QGroundControlQmlGlobal::AltitudeModeRelative); + _cameraCalc->setDistanceMode(_cameraCalc->distanceMode() == QGroundControlQmlGlobal::AltitudeFrameRelative + ? QGroundControlQmlGlobal::AltitudeFrameAbsolute + : QGroundControlQmlGlobal::AltitudeFrameRelative); QVERIFY(_cameraCalc->dirty()); _multiSpy->clearAllSignals(); _cameraCalc->setCameraBrand(CameraCalc::canonicalManualCameraName()); diff --git a/test/MissionManager/MissionControllerTest.cc b/test/MissionManager/MissionControllerTest.cc index bf5fc893eb17..9f30216871e6 100644 --- a/test/MissionManager/MissionControllerTest.cc +++ b/test/MissionManager/MissionControllerTest.cc @@ -371,24 +371,24 @@ void MissionControllerTest::_testLoadJsonSectionAvailable() } } -void MissionControllerTest::_testGlobalAltMode() +void MissionControllerTest::_testGlobalAltFrame() { _initForFirmwareType(MAV_AUTOPILOT_PX4); - struct _globalAltMode_s + struct _globalAltFrame_s { - QGroundControlQmlGlobal::AltMode altMode; + QGroundControlQmlGlobal::AltitudeFrame altFrame; MAV_FRAME expectedMavFrame; - } altModeTestCases[] = { - {QGroundControlQmlGlobal::AltitudeModeRelative, MAV_FRAME_GLOBAL_RELATIVE_ALT}, - {QGroundControlQmlGlobal::AltitudeModeAbsolute, MAV_FRAME_GLOBAL}, - {QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain, MAV_FRAME_GLOBAL}, - {QGroundControlQmlGlobal::AltitudeModeTerrainFrame, MAV_FRAME_GLOBAL_TERRAIN_ALT}, + } altFrameTestCases[] = { + {QGroundControlQmlGlobal::AltitudeFrameRelative, MAV_FRAME_GLOBAL_RELATIVE_ALT}, + {QGroundControlQmlGlobal::AltitudeFrameAbsolute, MAV_FRAME_GLOBAL}, + {QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain, MAV_FRAME_GLOBAL}, + {QGroundControlQmlGlobal::AltitudeFrameTerrain, MAV_FRAME_GLOBAL_TERRAIN_ALT}, }; - for (const _globalAltMode_s& testCase : altModeTestCases) { + for (const _globalAltFrame_s& testCase : altFrameTestCases) { _missionController->removeAll(); - _missionController->setGlobalAltitudeMode(testCase.altMode); + _missionController->setGlobalAltitudeFrame(testCase.altFrame); // Use coordinate fixtures const QGeoCoordinate start = Coord::zurich(); _missionController->insertTakeoffItem(start, 1); @@ -397,13 +397,13 @@ void MissionControllerTest::_testGlobalAltMode() _missionController->insertSimpleMissionItem(start.atDistanceAndAzimuth(300, 0), 4); SimpleMissionItem* si = qobject_cast(_missionController->visualItems()->value(1)); - QCOMPARE(si->altitudeMode(), QGroundControlQmlGlobal::AltitudeModeRelative); + QCOMPARE(si->altitudeFrame(), QGroundControlQmlGlobal::AltitudeFrameRelative); QCOMPARE(si->missionItem().frame(), MAV_FRAME_GLOBAL_RELATIVE_ALT); for (int i = 2; i < _missionController->visualItems()->count(); i++) { - TEST_DEBUG(QStringLiteral("Validating altitude mode index %1").arg(i)); + TEST_DEBUG(QStringLiteral("Validating altitude frame index %1").arg(i)); SimpleMissionItem* siLoop = qobject_cast(_missionController->visualItems()->value(i)); - QCOMPARE(siLoop->altitudeMode(), testCase.altMode); + QCOMPARE(siLoop->altitudeFrame(), testCase.altFrame); QCOMPARE(siLoop->missionItem().frame(), testCase.expectedMavFrame); } } diff --git a/test/MissionManager/MissionControllerTest.h b/test/MissionManager/MissionControllerTest.h index 3f4432eaa89d..042de6a30582 100644 --- a/test/MissionManager/MissionControllerTest.h +++ b/test/MissionManager/MissionControllerTest.h @@ -20,7 +20,7 @@ private slots: void cleanup() override; void _testLoadJsonSectionAvailable(); - void _testGlobalAltMode(); + void _testGlobalAltFrame(); void _testGimbalRecalc(); void _testVehicleYawRecalc(); void _testMissionReposition(); diff --git a/test/MissionManager/SimpleMissionItemTest.cc b/test/MissionManager/SimpleMissionItemTest.cc index 276520e569f7..b666ba5c9d14 100644 --- a/test/MissionManager/SimpleMissionItemTest.cc +++ b/test/MissionManager/SimpleMissionItemTest.cc @@ -57,19 +57,19 @@ void SimpleMissionItemTest::_testEditorFactsWorker(QGCMAVLink::VehicleClass_t ve MAV_CMD command; MAV_FRAME frame; double altValue; - QGroundControlQmlGlobal::AltMode altMode; + QGroundControlQmlGlobal::AltitudeFrame altFrame; } TestCase_t; TestCase_t testCases[] = { {MAV_CMD_NAV_WAYPOINT, MAV_FRAME_GLOBAL_RELATIVE_ALT, 70.1234567, - QGroundControlQmlGlobal::AltitudeModeRelative}, - {MAV_CMD_NAV_LOITER_UNLIM, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeModeAbsolute}, + QGroundControlQmlGlobal::AltitudeFrameRelative}, + {MAV_CMD_NAV_LOITER_UNLIM, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeFrameAbsolute}, {MAV_CMD_NAV_LOITER_TURNS, MAV_FRAME_GLOBAL_RELATIVE_ALT, 70.1234567, - QGroundControlQmlGlobal::AltitudeModeRelative}, - {MAV_CMD_NAV_LOITER_TIME, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeModeAbsolute}, - {MAV_CMD_NAV_LAND, MAV_FRAME_GLOBAL_RELATIVE_ALT, 70.1234567, QGroundControlQmlGlobal::AltitudeModeRelative}, - {MAV_CMD_NAV_TAKEOFF, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeModeAbsolute}, - {MAV_CMD_DO_JUMP, MAV_FRAME_MISSION, qQNaN(), QGroundControlQmlGlobal::AltitudeModeRelative}, + QGroundControlQmlGlobal::AltitudeFrameRelative}, + {MAV_CMD_NAV_LOITER_TIME, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeFrameAbsolute}, + {MAV_CMD_NAV_LAND, MAV_FRAME_GLOBAL_RELATIVE_ALT, 70.1234567, QGroundControlQmlGlobal::AltitudeFrameRelative}, + {MAV_CMD_NAV_TAKEOFF, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeFrameAbsolute}, + {MAV_CMD_DO_JUMP, MAV_FRAME_MISSION, qQNaN(), QGroundControlQmlGlobal::AltitudeFrameRelative}, }; PlanMasterController planController(MAV_AUTOPILOT_PX4, QGCMAVLink::vehicleClassToMavType(vehicleClass)); QGCMAVLink::VehicleClass_t commandVehicleClass = @@ -167,7 +167,7 @@ void SimpleMissionItemTest::_testEditorFactsWorker(QGCMAVLink::VehicleClass_t ve QCOMPARE(fact->rawValue().toDouble(), (cExpectedAdvancedNaNFieldInfo[j].first * 10.0) + 0.1234567); } if (!qIsNaN(testCase.altValue)) { - QCOMPARE(simpleMissionItem.altitudeMode(), testCase.altMode); + QCOMPARE(simpleMissionItem.altitudeFrame(), testCase.altFrame); QCOMPARE(simpleMissionItem.altitude()->rawValue().toDouble(), testCase.altValue); } } @@ -240,12 +240,12 @@ void SimpleMissionItemTest::_testSignals() missionItem.setParam1(missionItem.param4() + 1); QVERIFY(_spyVisualItem->emitted("dirtyChanged")); _spyVisualItem->clearAllSignals(); - // Changing altitude mode should emit these signals (may also emit other related signals) - _simpleItem->setAltitudeMode(_simpleItem->altitudeMode() == QGroundControlQmlGlobal::AltitudeModeRelative - ? QGroundControlQmlGlobal::AltitudeModeAbsolute - : QGroundControlQmlGlobal::AltitudeModeRelative); + // Changing altitude frame should emit these signals (may also emit other related signals) + _simpleItem->setAltitudeFrame(_simpleItem->altitudeFrame() == QGroundControlQmlGlobal::AltitudeFrameRelative + ? QGroundControlQmlGlobal::AltitudeFrameAbsolute + : QGroundControlQmlGlobal::AltitudeFrameRelative); QVERIFY(_spySimpleItem->emittedByMask( - _spySimpleItem->mask("dirtyChanged", "friendlyEditAllowedChanged", "altitudeModeChanged"))); + _spySimpleItem->mask("dirtyChanged", "friendlyEditAllowedChanged", "altitudeFrameChanged"))); _spySimpleItem->clearAllSignals(); _spyVisualItem->clearAllSignals(); // Check commandChanged signalling. Call setCommand should trigger: @@ -318,11 +318,11 @@ void SimpleMissionItemTest::_testSpeedSection() void SimpleMissionItemTest::_testAltitudePropogation() { // Make sure that changes to altitude propogate to param 7 of the mission item - _simpleItem->setAltitudeMode(QGroundControlQmlGlobal::AltitudeModeRelative); + _simpleItem->setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameRelative); _simpleItem->altitude()->setRawValue(_simpleItem->altitude()->rawValue().toDouble() + 1); QCOMPARE(_simpleItem->altitude()->rawValue().toDouble(), _simpleItem->missionItem().param7()); QCOMPARE(_simpleItem->missionItem().frame(), MAV_FRAME_GLOBAL_RELATIVE_ALT); - _simpleItem->setAltitudeMode(QGroundControlQmlGlobal::AltitudeModeAbsolute); + _simpleItem->setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameAbsolute); _simpleItem->altitude()->setRawValue(_simpleItem->altitude()->rawValue().toDouble() + 1); QCOMPARE(_simpleItem->altitude()->rawValue().toDouble(), _simpleItem->missionItem().param7()); QCOMPARE(_simpleItem->missionItem().frame(), MAV_FRAME_GLOBAL); diff --git a/test/UnitTestFramework/Fixtures/RAIIFixtures.cc b/test/UnitTestFramework/Fixtures/RAIIFixtures.cc index 7380adff5a15..54c5d9f3d4b4 100644 --- a/test/UnitTestFramework/Fixtures/RAIIFixtures.cc +++ b/test/UnitTestFramework/Fixtures/RAIIFixtures.cc @@ -150,11 +150,6 @@ void SettingsFixture::setOfflineVehicleType(MAV_TYPE vehicleType) appSettings->offlineEditingVehicleClass()->setRawValue(QGCMAVLink::vehicleClass(vehicleType)); } -void SettingsFixture::setAltitudeMode(int altitudeMode) -{ - Q_UNUSED(altitudeMode); -} - void SettingsFixture::setFactValue(Fact* fact, const QVariant& value) { if (!fact) { diff --git a/test/UnitTestFramework/Fixtures/RAIIFixtures.h b/test/UnitTestFramework/Fixtures/RAIIFixtures.h index 34d8a7269dcc..18c7f790f749 100644 --- a/test/UnitTestFramework/Fixtures/RAIIFixtures.h +++ b/test/UnitTestFramework/Fixtures/RAIIFixtures.h @@ -113,9 +113,6 @@ class SettingsFixture /// Set offline editing vehicle type void setOfflineVehicleType(MAV_TYPE vehicleType); - /// Set global altitude mode - void setAltitudeMode(int altitudeMode); - /// Set a Fact value (will be restored on destruction) void setFactValue(Fact* fact, const QVariant& value); From 7e83d28e0cc9b4654f65441e63630869412a8754 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 11 Mar 2026 13:36:49 -0700 Subject: [PATCH 070/119] Guard _visualItems.get(0) against empty model during initialization Add count checks before accessing _visualItems.get(0) in PlanView.qml to prevent warnings during the gap between QML construction and MissionController.start() populating the model. --- src/PlanView/PlanView.qml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/PlanView/PlanView.qml b/src/PlanView/PlanView.qml index 02c91f7bf53c..cd48235c1f65 100644 --- a/src/PlanView/PlanView.qml +++ b/src/PlanView/PlanView.qml @@ -164,7 +164,7 @@ Item { // Stop tracking map center when the home position is changed externally (e.g. drag, file load) Connections { - target: _visualItems.get(0) + target: _visualItems.count > 0 ? _visualItems.get(0) : null function onCoordinateChanged() { if (!_updatingHomeFromMapCenter && !_planMasterController.containsItems) { _homeTrackingMapCenter = false @@ -178,9 +178,11 @@ Item { function onContainsItemsChanged() { if (!_planMasterController.containsItems) { _homeTrackingMapCenter = true - _updatingHomeFromMapCenter = true - _visualItems.get(0).coordinate = editorMap.center - _updatingHomeFromMapCenter = false + if (_visualItems.count > 0) { + _updatingHomeFromMapCenter = true + _visualItems.get(0).coordinate = editorMap.center + _updatingHomeFromMapCenter = false + } } } } @@ -279,7 +281,7 @@ Item { } onCenterChanged: { QGroundControl.flightMapPosition = editorMap.center - if (_homeTrackingMapCenter && !_planMasterController.containsItems) { + if (_homeTrackingMapCenter && !_planMasterController.containsItems && _visualItems.count > 0) { _updatingHomeFromMapCenter = true _visualItems.get(0).coordinate = editorMap.center _updatingHomeFromMapCenter = false From 1bad343665142211898c421a1844d001564c8a04 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 11 Mar 2026 13:36:59 -0700 Subject: [PATCH 071/119] QGCTabBar: reset to first tab when becoming visible Ensures the tab selection resets when the tab bar becomes visible, preventing stale selection state when tabs are dynamically shown/hidden. Also calls _selectCurrentIndexButton() explicitly in case currentIndex is already 0 and onCurrentIndexChanged would not fire. --- src/QmlControls/QGCTabBar.qml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/QmlControls/QGCTabBar.qml b/src/QmlControls/QGCTabBar.qml index 2fc7d98f0bd4..e99d47844d8b 100644 --- a/src/QmlControls/QGCTabBar.qml +++ b/src/QmlControls/QGCTabBar.qml @@ -67,6 +67,15 @@ RowLayout { onCurrentIndexChanged: _selectCurrentIndexButton() onVisibleChildrenChanged: _selectCurrentIndexButton() + onVisibleChanged: { + if (visible) { + // When becoming visible, ensure the current index is valid and a button is selected + currentIndex = 0 + _selectCurrentIndexButton() + } + } + + ButtonGroup { id: buttonGroup From 21035804207778815285f87b3514567edc8ff968 Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Fri, 6 Feb 2026 15:46:03 -0500 Subject: [PATCH 072/119] feat(ntrip): rewrite NTRIP subsystem with transport abstraction and source table Complete rewrite of the NTRIP subsystem replacing the monolithic NTRIP.cc/h with a modular architecture: Transport layer: - NTRIPHttpTransport: raw TCP/TLS socket with HTTP/1.0 NTRIP handshake - NTRIPTransportConfig struct for clean parameter passing - RTCMParser: standalone RTCM3 frame parser with CRC24Q validation - RTCM whitelist filtering, NMEA checksum repair - Robust HTTP status parsing (handles ICY, SOURCETABLE, missing reason) - Named constants for timeouts and limits Manager: - Signal-driven settings (replaces 1s polling timer) - Exponential backoff reconnect with cancel support - Connect-on-start support - GGA position fallback chain extracted to _getBestPosition() - Live data rate tracking (bytes/sec) - Source table fetching with distance-sorted mountpoint browser - UDP forwarding support Settings UI: - Connect/Disconnect button with status indicator dot - Mountpoint browser with selected highlight, format/distance details - Password show/hide toggle, TLS checkbox - Data rate, message count, GGA source display - Wider text fields for better usability - HTML-stripped error messages from casters Tests: - NTRIPHttpTransportTest: HTTP parsing, whitelist, RTCM filtering, NMEA repair - NTRIPManagerTest: GGA generation format and coordinate coverage - NTRIPSourceTableTest: source table parsing and distance calculation - RTCMParserTest: frame parsing and CRC validation --- qgcresources.qrc | 7 - src/GPS/CMakeLists.txt | 7 +- src/GPS/NTRIP.cc | 1303 -------------------- src/GPS/NTRIP.h | 187 --- src/GPS/NTRIP/CMakeLists.txt | 18 + src/GPS/NTRIP/NTRIPHttpTransport.cc | 416 +++++++ src/GPS/NTRIP/NTRIPHttpTransport.h | 90 ++ src/GPS/NTRIP/NTRIPManager.cc | 630 ++++++++++ src/GPS/NTRIP/NTRIPManager.h | 149 +++ src/GPS/NTRIP/NTRIPSourceTable.cc | 202 +++ src/GPS/NTRIP/NTRIPSourceTable.h | 146 +++ src/GPS/NTRIP/RTCMParser.cc | 97 ++ src/GPS/NTRIP/RTCMParser.h | 42 + src/GPS/RTCMMavlink.cc | 2 - src/QmlControls/QGroundControlQmlGlobal.cc | 2 + src/QmlControls/QGroundControlQmlGlobal.h | 5 + src/Settings/NTRIP.SettingsGroup.json | 73 +- src/Settings/NTRIPSettings.cc | 63 +- src/Settings/NTRIPSettings.h | 5 +- src/UI/AppSettings/NTRIPSettings.qml | 342 ++++- test/CMakeLists.txt | 5 +- test/GPS/CMakeLists.txt | 10 +- test/GPS/NTRIPHttpTransportTest.cc | 303 +++++ test/GPS/NTRIPHttpTransportTest.h | 38 + test/GPS/NTRIPManagerTest.cc | 216 ++++ test/GPS/NTRIPManagerTest.h | 31 + test/GPS/NTRIPSourceTableTest.cc | 118 ++ test/GPS/NTRIPSourceTableTest.h | 17 + test/GPS/RTCMParserTest.cc | 351 ++++++ test/GPS/RTCMParserTest.h | 30 + tools/simulation/mock_ntrip_caster.py | 346 ++++++ 31 files changed, 3590 insertions(+), 1661 deletions(-) delete mode 100644 src/GPS/NTRIP.cc delete mode 100644 src/GPS/NTRIP.h create mode 100644 src/GPS/NTRIP/CMakeLists.txt create mode 100644 src/GPS/NTRIP/NTRIPHttpTransport.cc create mode 100644 src/GPS/NTRIP/NTRIPHttpTransport.h create mode 100644 src/GPS/NTRIP/NTRIPManager.cc create mode 100644 src/GPS/NTRIP/NTRIPManager.h create mode 100644 src/GPS/NTRIP/NTRIPSourceTable.cc create mode 100644 src/GPS/NTRIP/NTRIPSourceTable.h create mode 100644 src/GPS/NTRIP/RTCMParser.cc create mode 100644 src/GPS/NTRIP/RTCMParser.h create mode 100644 test/GPS/NTRIPHttpTransportTest.cc create mode 100644 test/GPS/NTRIPHttpTransportTest.h create mode 100644 test/GPS/NTRIPManagerTest.cc create mode 100644 test/GPS/NTRIPManagerTest.h create mode 100644 test/GPS/NTRIPSourceTableTest.cc create mode 100644 test/GPS/NTRIPSourceTableTest.h create mode 100644 test/GPS/RTCMParserTest.cc create mode 100644 test/GPS/RTCMParserTest.h create mode 100755 tools/simulation/mock_ntrip_caster.py diff --git a/qgcresources.qrc b/qgcresources.qrc index e16a29a5c49e..1a9a8de2352d 100644 --- a/qgcresources.qrc +++ b/qgcresources.qrc @@ -103,11 +103,4 @@ resources/gcscontrolIndicator/gcscontrol_gcs.svg resources/gcscontrolIndicator/gcscontrol_line.svg - - src/UI/AppSettings/NTRIPSettings.qml - src/UI/AppSettings/NTRIPSettings.qml - - - src/Settings/NTRIP.SettingsGroup.json - diff --git a/src/GPS/CMakeLists.txt b/src/GPS/CMakeLists.txt index 7a3aba76de22..902a62e6e255 100644 --- a/src/GPS/CMakeLists.txt +++ b/src/GPS/CMakeLists.txt @@ -18,8 +18,6 @@ target_sources(${CMAKE_PROJECT_NAME} GPSRtk.h GPSRTKFactGroup.cc GPSRTKFactGroup.h - NTRIP.cc - NTRIP.h RTCMMavlink.cc RTCMMavlink.h satellite_info.h @@ -31,6 +29,11 @@ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt6::Core) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +# ---------------------------------------------------------------------------- +# NTRIP Subsystem +# ---------------------------------------------------------------------------- +add_subdirectory(NTRIP) + # ============================================================================ # PX4 GPS Drivers Integration # ============================================================================ diff --git a/src/GPS/NTRIP.cc b/src/GPS/NTRIP.cc deleted file mode 100644 index 77b922c1f141..000000000000 --- a/src/GPS/NTRIP.cc +++ /dev/null @@ -1,1303 +0,0 @@ -#include "NTRIP.h" -#include "NTRIPSettings.h" -#include "Fact.h" -#include "FactGroup.h" -#include "QGCApplication.h" -#include "QGCLoggingCategory.h" -#include "RTCMMavlink.h" -#include "SettingsManager.h" -#include -#include - -#include "MultiVehicleManager.h" -#include "Vehicle.h" -#include "PositionManager.h" -#include -#include - -#include -#include -#include - -#include -#include - -QGC_LOGGING_CATEGORY(NTRIPLog, "qgc.ntrip") - -// Register the QML type without constructing the singleton up-front. -// This avoids creating NTRIPManager during a temporary Q(Core)Application -// used for command-line parsing, which could lead to it being destroyed -// early and leaving a dangling static pointer. -static QObject* _ntripManagerQmlProvider(QQmlEngine*, QJSEngine*) -{ - return NTRIPManager::instance(); -} - -static void _ntripManagerRegisterQmlTypes() -{ - qmlRegisterSingletonType("QGroundControl.NTRIP", 1, 0, "NTRIPManager", _ntripManagerQmlProvider); -} -Q_COREAPP_STARTUP_FUNCTION(_ntripManagerRegisterQmlTypes) - -NTRIPManager* NTRIPManager::_instance = nullptr; - -// constructor -NTRIPManager::NTRIPManager(QObject* parent) - : QObject(parent) - , _ntripStatus(tr("Disconnected")) -{ - qCDebug(NTRIPLog) << "NTRIPManager created"; - _startupTimer.start(); - - _rtcmMavlink = qgcApp() ? qgcApp()->findChild() : nullptr; - if (!_rtcmMavlink) { - QObject* parentObj = qgcApp() ? static_cast(qgcApp()) : static_cast(this); - // Ensure an RTCMMavlink helper exists for forwarding RTCM messages - _rtcmMavlink = new RTCMMavlink(parentObj); - _rtcmMavlink->setObjectName(QStringLiteral("RTCMMavlink")); - qCDebug(NTRIPLog) << "NTRIP Created RTCMMavlink helper"; - } - - // Force NTRIP checkbox OFF during app startup and keep it OFF until first settings tick. - { - NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); - if (settings && settings->ntripServerConnectEnabled()) { - Fact* fact = settings->ntripServerConnectEnabled(); - - // Force OFF immediately - fact->setRawValue(false); - - // While !_forcedOffOnce, if something tries to flip it ON, flip it back OFF. - _ntripEnableConn = connect(fact, &Fact::rawValueChanged, - this, [this, fact]() { - if (!_forcedOffOnce) { - const bool wantOn = fact->rawValue().toBool(); - if (wantOn) { - qCDebug(NTRIPLog) << "NTRIP Startup: coercing ntripServerConnectEnabled back to false"; - fact->setRawValue(false); - } - } - }); - } - } - - // Check settings periodically - _settingsCheckTimer = new QTimer(this); - connect(_settingsCheckTimer, &QTimer::timeout, this, &NTRIPManager::_checkSettings); - _settingsCheckTimer->start(1000); // Check every second - - _ggaTimer = new QTimer(this); - _ggaTimer->setInterval(5000); - connect(_ggaTimer, &QTimer::timeout, this, &NTRIPManager::_sendGGA); - - connect(qApp, &QCoreApplication::aboutToQuit, this, &NTRIPManager::stopNTRIP, Qt::QueuedConnection); - - _checkSettings(); -} - -// destructor -NTRIPManager::~NTRIPManager() -{ - qCDebug(NTRIPLog) << "NTRIPManager destroyed"; - stopNTRIP(); - // Clear singleton pointer in case we were destroyed (e.g., if created under - // a short-lived Q(Core)Application). - if (_instance == this) { - _instance = nullptr; - } -} - -RTCMParser::RTCMParser() -{ - reset(); -} - -void RTCMParser::reset() -{ - _state = WaitingForPreamble; - _messageLength = 0; - _bytesRead = 0; - _lengthBytesRead = 0; - _crcBytesRead = 0; -} - -bool RTCMParser::addByte(uint8_t byte) -{ - switch (_state) { - case WaitingForPreamble: - if (byte == RTCM3_PREAMBLE) { - _buffer[0] = byte; - _bytesRead = 1; - _state = ReadingLength; - _lengthBytesRead = 0; - } - break; - - case ReadingLength: - _lengthBytes[_lengthBytesRead++] = byte; - _buffer[_bytesRead++] = byte; - if (_lengthBytesRead == 2) { - // Extract 10-bit length from bytes (6 reserved bits + 10 length bits) - _messageLength = ((_lengthBytes[0] & 0x03) << 8) | _lengthBytes[1]; - if (_messageLength > 0 && _messageLength < 1021) { // Valid RTCM3 length - _state = ReadingMessage; - } else { - reset(); // Invalid length, restart - } - } - break; - - case ReadingMessage: - _buffer[_bytesRead++] = byte; - if (_bytesRead >= _messageLength + 3) { // +3 for header - _state = ReadingCRC; - _crcBytesRead = 0; - } - break; - - case ReadingCRC: - _crcBytes[_crcBytesRead++] = byte; - if (_crcBytesRead == 3) { - // Message complete - for simplicity, we'll skip CRC validation - return true; - } - break; - } - return false; -} - -uint16_t RTCMParser::messageId() -{ - if (_messageLength >= 2) { - // Message ID is in bits 14-25 of the message (after the 24-bit header) - return ((_buffer[3] << 4) | (_buffer[4] >> 4)) & 0xFFF; - } - return 0; -} - -NTRIPTCPLink::NTRIPTCPLink(const QString& hostAddress, - int port, - const QString& username, - const QString& password, - const QString& mountpoint, - const QString& whitelist, - bool useSpartn, - bool autoStart, - QObject* parent) - : QObject(parent) - , _hostAddress(hostAddress) - , _port(port) - , _username(username) - , _password(password) - , _mountpoint(mountpoint) - , _useSpartn(useSpartn) -{ - - - if (_useSpartn) { - // SPARTN path does not use RTCM whitelist or parser - if (!whitelist.isEmpty()) { - qCDebug(NTRIPLog) << "NTRIP SPARTN enabled; ignoring RTCM whitelist:" << whitelist; - } - // Initialize SPARTN header-strip guard - _spartnBuf.clear(); - _spartnNeedHeaderStrip = true; - - // Helpful hint if user left the default RTCM port - if (_port != 2102) { - qCWarning(NTRIPLog) << "NTRIP SPARTN is enabled but port is" << _port << "(expected 2102 for TLS)"; - } - } else { - // RTCM path: build whitelist and parser as before - for (const auto& msg : whitelist.split(',')) { - int msg_int = msg.toInt(); - if (msg_int) - _whitelist.append(msg_int); - } - qCDebug(NTRIPLog) << "NTRIP whitelist:" << _whitelist; - if (_whitelist.empty()) { - qCDebug(NTRIPLog) << "NTRIP whitelist is empty; all RTCM message IDs will be forwarded."; - } - if (!_rtcmParser) { - _rtcmParser = new RTCMParser(); - } - _rtcmParser->reset(); - } - - _state = NTRIPState::uninitialised; - - // Start only when requested so unit tests can exercise parser behavior without sockets. - if (autoStart) { - start(); - } -} - -NTRIPTCPLink::~NTRIPTCPLink() -{ - _stopping.store(true); - - if (_socket) { - QObject::disconnect(_readyReadConn); - _socket->disconnectFromHost(); - _socket->close(); - delete _socket; - _socket = nullptr; - } - - if (_rtcmParser) { - delete _rtcmParser; - _rtcmParser = nullptr; - } -} - -void NTRIPTCPLink::start() -{ - // This runs in the worker thread after QThread::started is emitted - _hardwareConnect(); -} - -void NTRIPTCPLink::_hardwareConnect() -{ - if (_stopping.load()) { - return; - } - - qCDebug(NTRIPLog) << "NTRIP connectToHost" << _hostAddress << ":" << _port << " mount=" << _mountpoint - << " tls=" << (_useSpartn ? "yes" : "no"); - - // allocate the appropriate socket type - // SPARTN selection (TLS on 2102 or when explicitly enabled) - if (_useSpartn) { - QSslSocket* sslSocket = new QSslSocket(this); - _socket = sslSocket; - } else { - _socket = new QTcpSocket(this); - } - - _socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); - _socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); - _socket->setReadBufferSize(0); - - QObject::connect(_socket, &QTcpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError code) { - // Suppress errors generated during intentional shutdown - if (_stopping.load() || !_socket) { - qCDebug(NTRIPLog) << "NTRIP suppressing socket error during intentional stop:" << int(code); - return; - } - - QString msg = _socket->errorString(); - - // If the peer closes before we receive any HTTP headers, try to give a helpful hint. - if (code == QAbstractSocket::RemoteHostClosedError && _state == NTRIPState::waiting_for_http_response) { - const QByteArray leftover = _socket->readAll(); - if (!leftover.isEmpty()) { - qCWarning(NTRIPLog) << "NTRIP peer closed; trailing bytes before close:" << leftover; - msg += QString(" (peer closed after %1 bytes)").arg(leftover.size()); - } else { - if (_port == 2102) { - msg += " (hint: port 2102 expects HTTPS/TLS; current socket is plain TCP)"; - } else if (!_mountpoint.isEmpty()) { - msg += " (peer closed before HTTP response; check mountpoint and credentials)"; - } - } - } - - qCWarning(NTRIPLog) << "NTRIP socket error code:" << int(code) - << " msg:" << msg - << " state:" << int(_socket->state()) - << " peer:" << _socket->peerAddress().toString() << ":" << _socket->peerPort() - << " local:" << _socket->localAddress().toString() << ":" << _socket->localPort(); - emit error(msg); - }, Qt::DirectConnection); - - // If the server cleanly disconnects, capture reason and emit error so NTRIPManager updates status - QObject::connect(_socket, &QTcpSocket::disconnected, this, [this]() { - // Suppress disconnect noise during intentional shutdown - if (_stopping.load() || !_socket) { - qCDebug(NTRIPLog) << "NTRIP suppressing disconnect signal during intentional stop"; - return; - } - - const QByteArray trailing = _socket->readAll(); - QString reason; - if (!trailing.isEmpty()) { - reason = QString::fromUtf8(trailing).trimmed(); - qCWarning(NTRIPLog) << "NTRIP disconnected; trailing bytes:" << trailing; - } else { - reason = "Server disconnected"; - qCWarning(NTRIPLog) << "NTRIP disconnected cleanly by server"; - } - // Emit richer context on disconnect - const qint64 t0 = this->property("ntrip_postok_t0_ms").toLongLong(); - const bool saw = this->property("ntrip_saw_rtcm").toBool(); - const qint64 dt = (t0 > 0) ? (QDateTime::currentMSecsSinceEpoch() - t0) : -1; - qCWarning(NTRIPLog) << "NTRIP disconnect context:" - << "rtcm_received_first_byte=" << (saw ? "yes" : "no") - << "ms_since_200=" << dt - << "peer=" << _socket->peerAddress().toString() << ":" << _socket->peerPort() - << "local=" << _socket->localAddress().toString() << ":" << _socket->localPort(); - emit error(reason); // Will call NTRIPManager::_tcpError() - }, Qt::DirectConnection); - - _readyReadConn = QObject::connect(_socket, &QTcpSocket::readyRead,this, &NTRIPTCPLink::_readBytes,Qt::DirectConnection); - - auto sendHttpRequest = [this]() { - if (!_mountpoint.isEmpty()) { - qCDebug(NTRIPLog) << "NTRIP Sending HTTP request"; - const QByteArray authB64 = QString(_username + ":" + _password).toUtf8().toBase64(); - QString query = - "GET /%1 HTTP/1.1\r\n" - "Host: %2\r\n" - "Ntrip-Version: Ntrip/2.0\r\n" - "User-Agent: QGC-NTRIP\r\n" - "Connection: keep-alive\r\n" - "Accept: */*\r\n" - "Authorization: Basic %3\r\n" - "\r\n"; - const QByteArray req = query.arg(_mountpoint).arg(_hostAddress).arg(QString::fromUtf8(authB64)).toUtf8(); - const qint64 written = _socket->write(req); - _socket->flush(); - - // Extra diagnostics: request line and auth presence (length only) - const QString reqStr = QString::fromUtf8(req); - const int cr = reqStr.indexOf("\r"); - const QString firstLine = (cr > 0) ? reqStr.left(cr).trimmed() : QString(); - const int authIdx = reqStr.indexOf("Authorization: Basic "); - int b64Len = -1; - if (authIdx >= 0) { - const int eol = reqStr.indexOf("\r\n", authIdx); - if (eol > authIdx) b64Len = eol - (authIdx + 21); - } - qCDebug(NTRIPLog) << "NTRIP HTTP request first line:" << firstLine; - qCDebug(NTRIPLog) << "NTRIP HTTP auth present:" << (authIdx >= 0) << "auth_b64_len:" << b64Len; - - qCDebug(NTRIPLog) << "NTRIP HTTP request bytes written:" << written; - _state = NTRIPState::waiting_for_http_response; - } - else { - _state = NTRIPState::waiting_for_rtcm_header; - emit connected(); - } - qCDebug(NTRIPLog) << "NTRIP Socket connected" - << "local" << _socket->localAddress().toString() << ":" << _socket->localPort() - << "-> peer" << _socket->peerAddress().toString() << ":" << _socket->peerPort(); - }; - - if (_useSpartn) { - QSslSocket* sslSocket = qobject_cast(_socket); - Q_ASSERT(sslSocket); - - QObject::connect(sslSocket, &QSslSocket::encrypted, this, [sendHttpRequest]() { - qCDebug(NTRIPLog) << "SPARTN TLS connection established"; - sendHttpRequest(); - }, Qt::DirectConnection); - - QObject::connect(sslSocket, QOverload&>::of(&QSslSocket::sslErrors), - this, [](const QList &errors) { - for (const QSslError &e : errors) { - qCWarning(NTRIPLog) << "TLS Error:" << e.errorString(); - } - }, Qt::DirectConnection); - - sslSocket->connectToHostEncrypted(_hostAddress, static_cast(_port)); - - if (!sslSocket->waitForEncrypted(10000)) { - qCDebug(NTRIPLog) << "NTRIP TLS socket failed to establish encryption"; - emit error(_socket->errorString()); - delete _socket; - _socket = nullptr; - return; - } - - if (sslSocket->isEncrypted()) { - sendHttpRequest(); - } - } else { - QTcpSocket* tcpSocket = qobject_cast(_socket); - Q_ASSERT(tcpSocket); - - tcpSocket->connectToHost(_hostAddress, static_cast(_port)); - - if (!tcpSocket->waitForConnected(10000)) { - qCDebug(NTRIPLog) << "NTRIP Socket failed to connect"; - emit error(_socket->errorString()); - delete _socket; - _socket = nullptr; - return; - } - - sendHttpRequest(); - } -} - -void NTRIPTCPLink::_parse(const QByteArray& buffer) -{ - if (_stopping.load()) { - return; - } - - static bool logged_empty_once = false; - if (!logged_empty_once && _whitelist.empty()) { - qCDebug(NTRIPLog) << "NTRIP whitelist is empty at runtime; forwarding all RTCM messages."; - logged_empty_once = true; - } - - // RTCM v3 transport sizes - constexpr int kRtcmHeaderSize = 3; // preamble (1) + length (2) - - for (char ch : buffer) { - const uint8_t byte = static_cast(static_cast(ch)); - - if (_state == NTRIPState::waiting_for_rtcm_header) { - if (byte != RTCM3_PREAMBLE) { - continue; - } - _state = NTRIPState::accumulating_rtcm_packet; - } - - if (_rtcmParser->addByte(byte)) { - _state = NTRIPState::waiting_for_rtcm_header; - - // Build exact on-wire frame: [D3 | len(2) | payload | CRC(3)] - const int payload_len = static_cast(_rtcmParser->messageLength()); - QByteArray message(reinterpret_cast(_rtcmParser->message()), - kRtcmHeaderSize + payload_len); - - const uint8_t* crc_ptr = _rtcmParser->crcBytes(); - const int crc_len = _rtcmParser->crcSize(); - message.append(reinterpret_cast(crc_ptr), crc_len); - - const uint16_t id = _rtcmParser->messageId(); - - if (_whitelist.empty() || _whitelist.contains(id)) { - qCDebug(NTRIPLog) << "NTRIP RTCM packet id" << id << "len" << message.length(); - emit RTCMDataUpdate(message); - qCDebug(NTRIPLog) << "NTRIP Sending" << id << "of size" << message.length(); - } else { - qCDebug(NTRIPLog) << "NTRIP Ignoring" << id; - } - - _rtcmParser->reset(); - } - } -} - -void NTRIPTCPLink::_handleSpartnData(const QByteArray& dataIn) -{ - if (dataIn.isEmpty()) { - return; - } - - // Accumulate so we can remove a response header exactly once if it ever appears here. - _spartnBuf.append(dataIn); - - if (_spartnNeedHeaderStrip) { - const bool looksHttp = _spartnBuf.startsWith("HTTP/") || _spartnBuf.startsWith("ICY "); - if (looksHttp) { - const int headerEnd = _spartnBuf.indexOf("\r\n\r\n"); - if (headerEnd < 0) { - // Header incomplete; cap buffer to avoid growth on a bad stream. - if (_spartnBuf.size() > 32768) { - _spartnBuf = _spartnBuf.right(32768); - } - return; - } - // Drop the header and keep the payload. - _spartnBuf.remove(0, headerEnd + 4); - } - _spartnNeedHeaderStrip = false; - } - - if (_spartnBuf.isEmpty()) { - return; - } - - // Deliver raw SPARTN bytes to whoever is consuming them (e.g., a GNSS forwarder). - emit SPARTNDataUpdate(_spartnBuf); - - // Clear after handing off. - _spartnBuf.clear(); -} - -void NTRIPTCPLink::_readBytes() -{ - if (_stopping.load()) { - return; - } - - // SPARTN path: after HTTP 200 OK, feed raw bytes through the SPARTN handler - if (_socket && _state == NTRIPState::waiting_for_spartn_data) { - const QByteArray bytes = _socket->readAll(); - if (!bytes.isEmpty()) { - _handleSpartnData(bytes); - } - return; - } - - if (!_socket) { - return; - } - - // Static counters and flags scoped to this function (no header changes needed) - static quint64 s_totalRtcm = 0; - - if (_state == NTRIPState::waiting_for_http_response) { - QByteArray responseData = _socket->readAll(); - if (responseData.isEmpty()) { - return; - } - - QString response = QString::fromUtf8(responseData); - qCDebug(NTRIPLog) << "NTRIP HTTP response received:" << response.left(200); - - // Dump full headers once for diagnostics - const int hdrEnd = response.indexOf("\r\n\r\n"); - const QString headers = (hdrEnd >= 0) ? response.left(hdrEnd) : response; - qCDebug(NTRIPLog) << "NTRIP HTTP response headers BEGIN >>>"; - for (const QString& line : headers.split("\r\n")) { - if (!line.isEmpty()) qCDebug(NTRIPLog) << line; - } - qCDebug(NTRIPLog) << "NTRIP HTTP response headers <<< END"; - - // Parse status line - QStringList lines = response.split('\n'); - bool foundOkResponse = false; - for (const QString& line : lines) { - const QString trimmed = line.trimmed(); - if ((trimmed.startsWith("HTTP/") || trimmed.startsWith("ICY ")) && - (trimmed.contains(" 200 ") || trimmed.contains(" 201 "))) { - foundOkResponse = true; - qCDebug(NTRIPLog) << "NTRIP: Found OK response:" << trimmed; - break; - } else if ((trimmed.startsWith("HTTP/") || trimmed.startsWith("ICY ")) && - (trimmed.contains(" 4") || trimmed.contains(" 5"))) { - qCWarning(NTRIPLog) << "NTRIP: Server error response:" << trimmed; - emit error(QString("NTRIP HTTP error: %1").arg(trimmed)); - - // Null out _socket before disconnectFromHost() so the - // synchronous 'disconnected' handler bails via !_socket guard. - QTcpSocket* sock = _socket; - _socket = nullptr; - _state = NTRIPState::uninitialised; - - QObject::disconnect(_readyReadConn); - sock->disconnectFromHost(); - sock->close(); - sock->deleteLater(); - - if (!_stopping.load()) { - QTimer::singleShot(3000, this, [this]() { - if (!_stopping.load()) { - _hardwareConnect(); - } - }); - } - return; - } - } - - if (foundOkResponse) { - // Mark the 200 OK time and reset first-RTCM flag using QObject properties. - const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); - this->setProperty("ntrip_postok_t0_ms", nowMs); - this->setProperty("ntrip_saw_rtcm", false); - this->setProperty("ntrip_watchdog_fired", false); - - // Fire a one-shot watchdog that only logs if no RTCM arrives by ~28s. - QTimer::singleShot(28000, this, [this]() { - if (this->property("ntrip_watchdog_fired").toBool()) return; - const bool saw = this->property("ntrip_saw_rtcm").toBool(); - if (!saw) { - this->setProperty("ntrip_watchdog_fired", true); - qCWarning(NTRIPLog) - << "NTRIP no RTCM received 28s after 200 OK. Likely caster timeout." - << "Check: duplicate login, entitlement/region, GGA validity."; - } - }); - - _state = _useSpartn ? NTRIPState::waiting_for_spartn_data - : NTRIPState::waiting_for_rtcm_header; - - qCDebug(NTRIPLog) << "NTRIP: HTTP handshake complete, transitioning to data state"; - emit connected(); - - // Process any remaining data after the headers - if (hdrEnd >= 0) { - QByteArray remainingData = responseData.mid(hdrEnd + 4); - if (!remainingData.isEmpty()) { - qCDebug(NTRIPLog) << "NTRIP: Processing data after HTTP headers:" << remainingData.size() << "bytes"; - if (_useSpartn) { - _handleSpartnData(remainingData); - } else { - _parse(remainingData); - } - } - } - } else { - qCDebug(NTRIPLog) << "NTRIP: Waiting for complete HTTP response..."; - } - - return; // done with HTTP response handling - } - - // Data after handshake (RTCM path) - QByteArray bytes = _socket->readAll(); - if (!bytes.isEmpty()) { - // Mark first RTCM arrival and log timing/sample - if (!this->property("ntrip_saw_rtcm").toBool()) { - this->setProperty("ntrip_saw_rtcm", true); - const qint64 t0 = this->property("ntrip_postok_t0_ms").toLongLong(); - const qint64 dt = (t0 > 0) ? (QDateTime::currentMSecsSinceEpoch() - t0) : -1; - const QByteArray sample = bytes.left(48); - const unsigned char b0 = static_cast(sample.isEmpty() ? 0 : sample[0]); - qCDebug(NTRIPLog) << "NTRIP first RTCM bytes at" << dt << "ms after 200 OK"; - qCDebug(NTRIPLog) << "NTRIP rx sample (hex, first 48B):" << sample.toHex(' '); - qCDebug(NTRIPLog) << "NTRIP preamble_check first_byte=0x" << QByteArray::number(b0, 16); - } - - s_totalRtcm += static_cast(bytes.size()); - qCDebug(NTRIPLog) << "NTRIP rx bytes:" << bytes.size() - << "total:" << s_totalRtcm - << "bytesAvailable:" << _socket->bytesAvailable(); - - _parse(bytes); - } -} - -void NTRIPTCPLink::debugFetchSourceTable() -{ - // Build request (include Authorization so the caster can tailor the table to your creds) - const QByteArray authB64 = QString(_username + ":" + _password).toUtf8().toBase64(); - const QByteArray req = - QByteArray("GET / HTTP/1.1\r\n") - + "Host: " + _hostAddress.toUtf8() + "\r\n" - + "User-Agent: QGC-NTRIP\r\n" - + "Ntrip-Version: Ntrip/2.0\r\n" - + "Accept: */*\r\n" - + "Authorization: Basic " + authB64 + "\r\n" - + "Connection: close\r\n\r\n"; - - QByteArray all; - - if (_useSpartn || _port == 2102) { - // TLS (e.g., SPARTN/2102) - QSslSocket sock; - sock.connectToHostEncrypted(_hostAddress, static_cast(_port)); - if (!sock.waitForEncrypted(7000)) { - qCWarning(NTRIPLog) << "SOURCETABLE TLS connect failed:" << sock.errorString(); - return; - } - if (sock.write(req) <= 0 || !sock.waitForBytesWritten(3000)) { - qCWarning(NTRIPLog) << "SOURCETABLE TLS write failed:" << sock.errorString(); - return; - } - while (sock.waitForReadyRead(3000)) { - all += sock.readAll(); - if (sock.bytesAvailable() == 0 && sock.state() != QAbstractSocket::ConnectedState) break; - } - sock.close(); - } else { - // Plain TCP (e.g., RTCM/2101) - QTcpSocket sock; - sock.connectToHost(_hostAddress, static_cast(_port)); - if (!sock.waitForConnected(5000)) { - qCWarning(NTRIPLog) << "SOURCETABLE connect failed:" << sock.errorString(); - return; - } - if (sock.write(req) <= 0 || !sock.waitForBytesWritten(3000)) { - qCWarning(NTRIPLog) << "SOURCETABLE write failed:" << sock.errorString(); - return; - } - while (sock.waitForReadyRead(3000)) { - all += sock.readAll(); - if (sock.bytesAvailable() == 0 && sock.state() != QAbstractSocket::ConnectedState) break; - } - sock.close(); - } - - // Split headers/body and dump the body (the sourcetable) - const int hdrEnd = all.indexOf("\r\n\r\n"); - const QByteArray body = (hdrEnd >= 0) ? all.mid(hdrEnd + 4) : all; - - qCDebug(NTRIPLog) << "----- NTRIP SOURCETABLE BEGIN -----"; - for (const QByteArray& line : body.split('\n')) { - const QByteArray l = line.trimmed(); - if (!l.isEmpty()) qCDebug(NTRIPLog) << l; - } - qCDebug(NTRIPLog) << "------ NTRIP SOURCETABLE END ------"; -} - -void NTRIPTCPLink::sendNMEA(const QByteArray& sentence) -{ - if (_stopping.load()) { - return; - } - if (!_socket || _socket->state() != QAbstractSocket::ConnectedState) { - return; - } - - QByteArray line = sentence; - - // Validate or repair checksum in the form $CORE*XX - if (line.size() >= 5 && line.at(0) == '$') { - int star = line.lastIndexOf('*'); - if (star > 1) { - // Calculate checksum over bytes between '$' and '*' - quint8 calc = 0; - for (int i = 1; i < star; ++i) { - calc ^= static_cast(line.at(i)); - } - - // Format calculated checksum as two uppercase hex digits - QByteArray calcCks = QByteArray::number(calc, 16) - .rightJustified(2, '0') - .toUpper(); - - bool needsRepair = false; - if (star + 3 > line.size()) { - // Not enough chars after '*', definitely needs repair - needsRepair = true; - } else { - QByteArray txCks = line.mid(star + 1, 2).toUpper(); - if (txCks != calcCks) { - needsRepair = true; - } - } - - if (needsRepair) { - // Remove any existing checksum and replace with correct one - line = line.left(star + 1) + calcCks; - } - } else { - // No '*' found, append one and correct checksum - quint8 calc = 0; - for (int i = 1; i < line.size(); ++i) { - calc ^= static_cast(line.at(i)); - } - QByteArray calcCks = QByteArray::number(calc, 16) - .rightJustified(2, '0') - .toUpper(); - line.append('*').append(calcCks); - } - } - - // Ensure CRLF termination - if (!line.endsWith("\r\n")) { - line.append("\r\n"); - } - - qCDebug(NTRIPLog) << "NTRIP Sent NMEA:" << QString::fromUtf8(line.trimmed()); - - const qint64 written = _socket->write(line); - _socket->flush(); - - qCDebug(NTRIPLog) << "NTRIP Socket state:" << (_socket ? _socket->state() : -1); - qCDebug(NTRIPLog) << "NTRIP Bytes written:" << written; -} - - -void NTRIPTCPLink::requestStop() -{ - _stopping.store(true); - - if (_socket) { - QObject::disconnect(_readyReadConn); - _socket->disconnectFromHost(); - _socket->close(); - delete _socket; - _socket = nullptr; - } - - emit finished(); // let NTRIPManager's QThread quit in response -} - -NTRIPManager* NTRIPManager::instance() -{ - if (!_instance) { - // Prefer QGCApplication as parent if available, otherwise fall back to qApp - QObject* parent = qgcApp() ? static_cast(qgcApp()) : static_cast(qApp); - _instance = new NTRIPManager(parent); - } - return _instance; -} - -void NTRIPManager::startNTRIP() -{ - if (_startStopBusy) { - return; - } - _startStopBusy = true; // guard begin - - qCDebug(NTRIPLog) << "startNTRIP: begin"; - - if (_tcpLink) { - _startStopBusy = false; // already started; release guard - return; - } - - _ntripStatus = tr("Connecting..."); - emit ntripStatusChanged(); - - - NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); - QString host; - int port = 2101; - QString user; - QString pass; - QString mount; - QString whitelist; - - if (settings) { - if (settings->ntripServerHostAddress()) host = settings->ntripServerHostAddress()->rawValue().toString(); - if (settings->ntripServerPort()) port = settings->ntripServerPort()->rawValue().toInt(); - if (settings->ntripUsername()) user = settings->ntripUsername()->rawValue().toString(); - if (settings->ntripPassword()) pass = settings->ntripPassword()->rawValue().toString(); - if (settings->ntripMountpoint()) mount = settings->ntripMountpoint()->rawValue().toString(); - if (settings->ntripWhitelist()) whitelist = settings->ntripWhitelist()->rawValue().toString(); - if (settings->ntripUseSpartn()) _useSpartn = settings->ntripUseSpartn()->rawValue().toBool(); - } else { - qCWarning(NTRIPLog) << "NTRIP settings group is null; starting with defaults"; - } - - qCDebug(NTRIPLog) << "startNTRIP: host=" << host - << " port=" << port - << " mount=" << mount - << " userIsEmpty=" << user.isEmpty(); - - _tcpThread = new QThread(this); - NTRIPTCPLink* link = new NTRIPTCPLink(host, port, user, pass, mount, whitelist, _useSpartn); - _tcpLink = link; - - link->moveToThread(_tcpThread); - - connect(_tcpThread, &QThread::started, link, &NTRIPTCPLink::start); - connect(link, &NTRIPTCPLink::finished, _tcpThread, &QThread::quit); - connect(_tcpThread, &QThread::finished, _tcpThread, &QObject::deleteLater); - connect(_tcpThread, &QThread::finished, link, &QObject::deleteLater); - - // Surface errors (including server disconnect reasons) to status/UI - connect(_tcpLink, &NTRIPTCPLink::error, - this, &NTRIPManager::_tcpError, - Qt::QueuedConnection); - - connect(_tcpLink, &NTRIPTCPLink::connected, this, [this]() { - _casterStatus = CasterStatus::Connected; - emit casterStatusChanged(_casterStatus); - - const QString want = _useSpartn ? tr("Connected (SPARTN)") : tr("Connected"); - if (_ntripStatus != want) { - _ntripStatus = want; - emit ntripStatusChanged(); - } - - // >>> ONE-SHOT SOURCETABLE DUMP (enable with env var QGC_NTRIP_DUMP_TABLE=1) - if (qEnvironmentVariableIsSet("QGC_NTRIP_DUMP_TABLE")) { - static bool dumped = false; - if (!dumped && _tcpLink) { - dumped = true; - QMetaObject::invokeMethod(_tcpLink, "debugFetchSourceTable", Qt::QueuedConnection); - } - } - // <<< - - // Start rapid 1 Hz GGA until we get the first valid coord - if (_ggaTimer && !_ggaTimer->isActive()) { - _ggaTimer->setInterval(1000); // 1 Hz while waiting for fix - _sendGGA(); // try immediately - _ggaTimer->start(); - } - - }, Qt::QueuedConnection); - - if (_useSpartn) { - connect(_tcpLink, &NTRIPTCPLink::SPARTNDataUpdate, this, [this](const QByteArray& data){ - static uint32_t spartn_count = 0; - if ((spartn_count++ % 50) == 0) { - qCDebug(NTRIPLog) << "SPARTN bytes flowing:" << data.size(); - } - if (_ntripStatus != tr("Connected (SPARTN)")) { - _ntripStatus = tr("Connected (SPARTN)"); - emit ntripStatusChanged(); - } - _casterStatus = CasterStatus::Connected; - emit casterStatusChanged(_casterStatus); - - _rtcmDataReceived(data); - }, Qt::QueuedConnection); - } else { - connect(_tcpLink, &NTRIPTCPLink::RTCMDataUpdate, this, &NTRIPManager::_rtcmDataReceived, Qt::QueuedConnection); - } - - _tcpThread->start(); - qCDebug(NTRIPLog) << "NTRIP started"; - - _startStopBusy = false; // guard end -} - -void NTRIPManager::stopNTRIP() -{ - if (_startStopBusy) { - return; - } - _startStopBusy = true; // guard begin - - if (_tcpLink) { - // Prevent spurious "NTRIP error: ..." toasts during user-initiated shutdown - disconnect(_tcpLink, &NTRIPTCPLink::error, this, &NTRIPManager::_tcpError); - - // Ask the worker to stop in its own thread - QMetaObject::invokeMethod(_tcpLink, "requestStop", Qt::QueuedConnection); - - // If we still own the thread, stop and wait for it - if (_tcpThread) { - _tcpThread->quit(); // or just rely on 'finished' from requestStop() - _tcpThread->wait(); - _tcpThread = nullptr; - } - - // _tcpLink will be deleted via deleteLater() when the thread finishes - _tcpLink = nullptr; - - _ntripStatus = tr("Disconnected"); - emit ntripStatusChanged(); - qCDebug(NTRIPLog) << "NTRIP stopped"; - } - - if (_ggaTimer && _ggaTimer->isActive()) { - _ggaTimer->stop(); - } - - _startStopBusy = false; // guard end -} - -void NTRIPManager::_tcpError(const QString& errorMsg) -{ - qCWarning(NTRIPLog) << "NTRIP Server Error:" << errorMsg; - _ntripStatus = errorMsg; - emit ntripStatusChanged(); - _startStopBusy = false; // make sure guard clears on error - - // Check for specific "no location provided" disconnect reason - if (errorMsg.contains("NO_LOCATION_PROVIDED", Qt::CaseInsensitive)) { - _onCasterDisconnected(errorMsg); - } else { - _casterStatus = CasterStatus::OtherError; - emit casterStatusChanged(_casterStatus); - } - - // Show error to user via QGC notification (toast) - if (qgcApp()) { - qgcApp()->showAppMessage(tr("NTRIP error: %1").arg(errorMsg)); - } -} - -void NTRIPManager::_rtcmDataReceived(const QByteArray& data) -{ - - qCDebug(NTRIPLog) << "NTRIP Forwarding RTCM to vehicle:" << data.size() << "bytes"; - - // Lazily resolve the tool if we don't have it yet - if (!_rtcmMavlink && qgcApp()) { - _rtcmMavlink = qgcApp()->findChild(); - } - - if (_rtcmMavlink) { - // Forward raw RTCM bytes to the autopilot via MAVLink GPS_RTCM_DATA - _rtcmMavlink->RTCMDataUpdate(data); - - if (_ntripStatus != tr("Connected")) { - _ntripStatus = tr("Connected"); - emit ntripStatusChanged(); - } - } else { - // Tool not constructed yet — avoid crash, keep status, and don't spam - static uint32_t drop_warn_counter = 0; - if ((drop_warn_counter++ % 50) == 0) { - qCWarning(NTRIPLog) << "RTCMMavlink tool not ready; dropping RTCM bytes:" << data.size(); - } - } - - _startStopBusy = false; // guard clears once bytes flow (or we tried) -} - -static QByteArray _makeGGA(const QGeoCoordinate& coord, double altitude_msl) -{ - // UTC time hhmmss format - const QTime utc = QDateTime::currentDateTimeUtc().time(); - const QString hhmmss = QString("%1%2%3") - .arg(utc.hour(), 2, 10, QChar('0')) - .arg(utc.minute(), 2, 10, QChar('0')) - .arg(utc.second(), 2, 10, QChar('0')); - - // Convert decimal degrees to NMEA DMM with zero-padded minutes: - // lat: ddmm.mmmm, lon: dddmm.mmmm - auto dmm = [](double deg, bool lat) -> QString { - double a = qFabs(deg); - int d = int(a); - double m = (a - d) * 60.0; - - // Round to 4 decimals and handle 59.99995 -> 60.0000 carry into degrees - int m10000 = int(m * 10000.0 + 0.5); - double m_rounded = m10000 / 10000.0; - if (m_rounded >= 60.0) { - m_rounded -= 60.0; - d += 1; - } - - // Ensure minutes have two digits before the decimal (e.g. 08.1234) - QString mm = QString::number(m_rounded, 'f', 4); - if (m_rounded < 10.0) { - mm.prepend("0"); - } - - if (lat) { - return QString("%1%2").arg(d, 2, 10, QChar('0')).arg(mm); - } else { - return QString("%1%2").arg(d, 3, 10, QChar('0')).arg(mm); - } - }; - - const bool latNorth = coord.latitude() >= 0.0; - const bool lonEast = coord.longitude() >= 0.0; - - const QString latField = dmm(coord.latitude(), true); - const QString lonField = dmm(coord.longitude(), false); - - // IMPORTANT: include geoid separation as 0.0 and unit 'M' - // Fields: time,lat,N,lon,E,fix,nsat,hdop,alt,M,geoid,M,age,station - const QString core = QString("GPGGA,%1,%2,%3,%4,%5,1,12,1.0,%6,M,0.0,M,,") - .arg(hhmmss) - .arg(latField) - .arg(latNorth ? "N" : "S") - .arg(lonField) - .arg(lonEast ? "E" : "W") - .arg(QString::number(altitude_msl, 'f', 1)); - - // NMEA checksum over bytes between '$' and '*' - quint8 cksum = 0; - const QByteArray coreBytes = core.toUtf8(); - for (char ch : coreBytes) { - cksum ^= static_cast(ch); - } - - const QString cks = QString("%1").arg(cksum, 2, 16, QChar('0')).toUpper(); - const QByteArray sentence = QByteArray("$") + coreBytes + QByteArray("*") + cks.toUtf8(); - return sentence; -} - -void NTRIPManager::_sendGGA() -{ - if (!_tcpLink) { - return; - } - - QGeoCoordinate coord; - double alt_msl = 0.0; - bool validCoord = false; - QString srcUsed; - - if (qgcApp()) { - // PRIORITY 1: Raw GPS data from vehicle GPS facts (most important for RTK) - MultiVehicleManager* mvm = MultiVehicleManager::instance(); - if (mvm) { - if (Vehicle* veh = mvm->activeVehicle()) { - FactGroup* gps = veh->gpsFactGroup(); - if (gps) { - // Try common GPS fact names - Fact* latF = gps->getFact(QStringLiteral("lat")); - Fact* lonF = gps->getFact(QStringLiteral("lon")); - - // If "lat"/"lon" don't work, try alternative names - if (!latF) latF = gps->getFact(QStringLiteral("latitude")); - if (!lonF) lonF = gps->getFact(QStringLiteral("longitude")); - if (!latF) latF = gps->getFact(QStringLiteral("lat_deg")); - if (!lonF) lonF = gps->getFact(QStringLiteral("lon_deg")); - if (!latF) latF = gps->getFact(QStringLiteral("latitude_deg")); - if (!lonF) lonF = gps->getFact(QStringLiteral("longitude_deg")); - - if (latF && lonF) { - const double glat = latF->rawValue().toDouble(); - const double glon = lonF->rawValue().toDouble(); - - // GPS coordinates might be in 1e-7 degrees format (int32 scaled) - double lat_deg = glat; - double lon_deg = glon; - - // Check if values look like scaled integers (> 1000 suggests 1e-7 format) - if (qAbs(glat) > 1000.0 || qAbs(glon) > 1000.0) { - lat_deg = glat * 1e-7; - lon_deg = glon * 1e-7; - } - - if (qIsFinite(lat_deg) && qIsFinite(lon_deg) && - !(lat_deg == 0.0 && lon_deg == 0.0) && - qAbs(lat_deg) <= 90.0 && qAbs(lon_deg) <= 180.0) { - - coord = QGeoCoordinate(lat_deg, lon_deg); - validCoord = true; - - // Get altitude from GPS if available - Fact* altF = gps->getFact(QStringLiteral("alt")); - if (!altF) altF = gps->getFact(QStringLiteral("altitude")); - if (altF) { - double raw_alt = altF->rawValue().toDouble(); - // Altitude might be in mm or cm, convert to meters - if (qAbs(raw_alt) > 10000.0) { // > 10km suggests mm format - alt_msl = raw_alt * 1e-3; // mm to meters - } else if (qAbs(raw_alt) > 1000.0) { // > 1km suggests cm format - alt_msl = raw_alt * 1e-2; // cm to meters - } else { - alt_msl = raw_alt; // already in meters - } - if (!qIsFinite(alt_msl)) alt_msl = 0.0; - } - - srcUsed = QStringLiteral("GPS Raw"); - qCDebug(NTRIPLog) << "NTRIP: Using raw GPS data for GGA" - << "lat:" << lat_deg << "lon:" << lon_deg << "alt:" << alt_msl; - } - } - } else { - // Vehicle exists but no GPS FactGroup yet - qCDebug(NTRIPLog) << "NTRIP: Vehicle found but no GPS FactGroup available yet"; - } - } else { - // MultiVehicleManager exists but no active vehicle yet - static int no_vehicle_count = 0; - if ((no_vehicle_count++ % 10) == 0) { // Log every 10th time to avoid spam - qCDebug(NTRIPLog) << "NTRIP: MultiVehicleManager found but no active vehicle yet"; - } - } - } else { - // MultiVehicleManager not found yet - this is normal during early startup - static int no_mvm_count = 0; - if ((no_mvm_count++ % 10) == 0) { // Log every 10th time to avoid spam - qCDebug(NTRIPLog) << "NTRIP: MultiVehicleManager not available yet (startup)"; - } - } - - // PRIORITY 2: Vehicle global position estimate (EKF output) - if (!validCoord) { - MultiVehicleManager* mvm2 = MultiVehicleManager::instance(); - if (mvm2) { - if (Vehicle* veh2 = mvm2->activeVehicle()) { - coord = veh2->coordinate(); - if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) { - validCoord = true; - double a = coord.altitude(); - alt_msl = qIsFinite(a) ? a : 0.0; - srcUsed = QStringLiteral("Vehicle EKF"); - qCDebug(NTRIPLog) << "NTRIP: Using vehicle EKF position for GGA" << coord; - } - } - } - } - - } - - if (!validCoord) { - qCDebug(NTRIPLog) << "NTRIP: No valid position available, skipping GGA."; - return; - } - - // Once we have a real valid coord, slow down GGA to 5 seconds - if (_ggaTimer && _ggaTimer->interval() != 5000) { - _ggaTimer->setInterval(5000); - qCDebug(NTRIPLog) << "NTRIP: Real position acquired, reducing GGA frequency to 5s"; - } - - const QByteArray gga = _makeGGA(coord, alt_msl); - - // Debug: Log the GGA sentence being sent (but reduce spam) - static int gga_send_count = 0; - if ((gga_send_count++ % 5) == 0) { // Log every 5th GGA to reduce spam - qCDebug(NTRIPLog) << "NTRIP: Sending GGA:" << gga; - qCDebug(NTRIPLog) << "NTRIP: GGA coord:" << coord << "alt:" << alt_msl << "source:" << srcUsed; - } - - QMetaObject::invokeMethod(_tcpLink, "sendNMEA", - Qt::QueuedConnection, - Q_ARG(QByteArray, gga)); - - // Update GGA source for UI display - if (!srcUsed.isEmpty() && srcUsed != _ggaSource) { - _ggaSource = srcUsed; - emit ggaSourceChanged(); - } -} - -void NTRIPManager::_checkSettings() -{ - if (_startStopBusy) { - qCDebug(NTRIPLog) << "NTRIP _checkSettings: busy, skipping"; - return; - } - - NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); - Fact* enableFact = nullptr; - if (settings && settings->ntripServerConnectEnabled()) { - enableFact = settings->ntripServerConnectEnabled(); - } - - // During startup, keep forcing OFF until we observe the Fact stable at false - // for 2 consecutive ticks. This defeats late restores from QML/settings. - if (_startupSuppress) { - if (enableFact) { - if (enableFact->rawValue().toBool()) { - qCDebug(NTRIPLog) << "NTRIP Startup: coercing OFF (late restore detected)"; - enableFact->setRawValue(false); - _startupStableTicks = 0; // reset stability counter on any true - } else { - _startupStableTicks++; - } - } else { - _startupStableTicks = 0; // no fact yet, not stable - } - - // Release once we have seen false for 2 ticks - if (_startupStableTicks >= 2) { - _startupSuppress = false; - if (!_forcedOffOnce) { - _forcedOffOnce = true; - if (_ntripEnableConn) { - disconnect(_ntripEnableConn); - } - } - qCDebug(NTRIPLog) << "NTRIP Startup: suppression released; honoring user setting from now on"; - } else { - return; // keep suppressing this tick - } - } - - bool shouldBeRunning = false; - if (enableFact) { - shouldBeRunning = enableFact->rawValue().toBool(); - } else { - qCWarning(NTRIPLog) << "NTRIP settings missing; defaulting to disabled"; - } - - bool isRunning = (_tcpLink != nullptr); - - qCDebug(NTRIPLog) << "NTRIP _checkSettings:" - << " isRunning=" << isRunning - << " shouldBeRunning=" << shouldBeRunning; - - if (shouldBeRunning && !isRunning) { - qCDebug(NTRIPLog) << "NTRIP _checkSettings: calling startNTRIP()"; - startNTRIP(); - } else if (!shouldBeRunning && isRunning) { - qCDebug(NTRIPLog) << "NTRIP _checkSettings: calling stopNTRIP()"; - stopNTRIP(); - } -} - -void NTRIPManager::_onCasterDisconnected(const QString& reason) -{ - qWarning() << "NTRIP: Disconnected by server:" << reason; - if (reason.contains("NO_LOCATION_PROVIDED", Qt::CaseInsensitive)) { - _casterStatus = CasterStatus::NoLocationError; - } else { - _casterStatus = CasterStatus::OtherError; - } - emit casterStatusChanged(_casterStatus); -} diff --git a/src/GPS/NTRIP.h b/src/GPS/NTRIP.h deleted file mode 100644 index 5c580af0b3d8..000000000000 --- a/src/GPS/NTRIP.h +++ /dev/null @@ -1,187 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include - -Q_DECLARE_LOGGING_CATEGORY(NTRIPLog) - -class RTCMMavlink; -class NTRIPSettings; -class Vehicle; -class MultiVehicleManager; -class GpsTest; - -#define RTCM3_PREAMBLE 0xD3 - -class RTCMParser -{ -public: - RTCMParser(); - void reset(); - bool addByte(uint8_t byte); - uint8_t* message() { return _buffer; } - uint16_t messageLength() { return _messageLength; } - uint16_t messageId(); - const uint8_t* crcBytes() const { return _crcBytes; } - int crcSize() const { return 3; } - -private: - enum State { - WaitingForPreamble, - ReadingLength, - ReadingMessage, - ReadingCRC - }; - - State _state; - uint8_t _buffer[1024]; - uint16_t _messageLength; - uint16_t _bytesRead; - uint16_t _lengthBytesRead; - uint8_t _lengthBytes[2]; - uint16_t _crcBytesRead; - uint8_t _crcBytes[3]; -}; - -class NTRIPTCPLink : public QObject -{ - Q_OBJECT - -public: -#ifdef QGC_UNITTEST_BUILD - friend class GpsTest; -#endif - - NTRIPTCPLink(const QString& hostAddress, - int port, - const QString& username, - const QString& password, - const QString& mountpoint, - const QString& whitelist, - bool useSpartn, - bool autoStart = true, - QObject* parent = nullptr); - - Q_INVOKABLE void debugFetchSourceTable(); - - ~NTRIPTCPLink(); - -public slots: - void start(); - void requestStop(); - void sendNMEA(const QByteArray& sentence); - -signals: - void finished(); - void error(const QString& errorMsg); - void RTCMDataUpdate(const QByteArray& message); - // Called when SPARTN corrections are received from the NTRIPTCPLink - // These corrections are forwarded to the connected vehicle (PX4/ArduPilot) - // using the same path as RTCM corrections via _rtcmDataReceived(). - void SPARTNDataUpdate(const QByteArray& message); - void connected(); - - -private slots: - void _readBytes(); - -private: - enum class NTRIPState { - uninitialised, - waiting_for_http_response, - waiting_for_spartn_data, - waiting_for_rtcm_header, - accumulating_rtcm_packet, - }; - - void _hardwareConnect(); - void _parse(const QByteArray& buffer); - void _handleSpartnData(const QByteArray& data); - - QTcpSocket* _socket = nullptr; - - QString _hostAddress; - int _port; - QString _username; - QString _password; - QString _mountpoint; - bool _useSpartn = false; - QVector _whitelist; - - RTCMParser* _rtcmParser = nullptr; - NTRIPState _state; - - std::atomic _stopping{false}; - QMetaObject::Connection _readyReadConn; - - // Small buffer to strip a response header only once if needed - QByteArray _spartnBuf; - bool _spartnNeedHeaderStrip = true; - -}; - -class NTRIPManager : public QObject -{ - Q_OBJECT - Q_PROPERTY(QString ntripStatus READ ntripStatus NOTIFY ntripStatusChanged) - Q_PROPERTY(CasterStatus casterStatus READ casterStatus NOTIFY casterStatusChanged) - Q_PROPERTY(QString ggaSource READ ggaSource NOTIFY ggaSourceChanged) - -public: - enum class CasterStatus { Connected, NoLocationError, OtherError }; - - NTRIPManager(QObject* parent = nullptr); - ~NTRIPManager(); - - static NTRIPManager* instance(); - - QString ntripStatus() const { return _ntripStatus; } - CasterStatus casterStatus() const { return _casterStatus; } - QString ggaSource() const { return _ggaSource; } - - void startNTRIP(); - void stopNTRIP(); - -signals: - void ntripStatusChanged(); - void casterStatusChanged(CasterStatus status); - void ggaSourceChanged(); - -public slots: - void _tcpError(const QString& errorMsg); - void _rtcmDataReceived(const QByteArray& data); - -private: - void _checkSettings(); - void _sendGGA(); - void _onCasterDisconnected(const QString& reason); - - QTimer* _ggaTimer = nullptr; - - QString _ntripStatus; - QString _ggaSource; - QMetaObject::Connection _ntripEnableConn; - - NTRIPTCPLink* _tcpLink = nullptr; - QThread* _tcpThread = nullptr; - RTCMMavlink* _rtcmMavlink = nullptr; - QTimer* _settingsCheckTimer = nullptr; - bool _startStopBusy = false; - bool _forcedOffOnce = false; - bool _useSpartn = false; - - QElapsedTimer _startupTimer; - bool _startupSuppress = true; - int _startupStableTicks = 0; - - CasterStatus _casterStatus = CasterStatus::OtherError; - - static NTRIPManager* _instance; -}; diff --git a/src/GPS/NTRIP/CMakeLists.txt b/src/GPS/NTRIP/CMakeLists.txt new file mode 100644 index 000000000000..e8a891d846f5 --- /dev/null +++ b/src/GPS/NTRIP/CMakeLists.txt @@ -0,0 +1,18 @@ +# ============================================================================ +# NTRIP Module +# NTRIP client, RTCM parsing, and correction streaming +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + NTRIPHttpTransport.cc + NTRIPHttpTransport.h + NTRIPManager.cc + NTRIPManager.h + NTRIPSourceTable.cc + NTRIPSourceTable.h + RTCMParser.cc + RTCMParser.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/GPS/NTRIP/NTRIPHttpTransport.cc b/src/GPS/NTRIP/NTRIPHttpTransport.cc new file mode 100644 index 000000000000..5a202dba1542 --- /dev/null +++ b/src/GPS/NTRIP/NTRIPHttpTransport.cc @@ -0,0 +1,416 @@ +#include "NTRIPHttpTransport.h" +#include "QGCLoggingCategory.h" + +#include +#include +#include +#include + +QGC_LOGGING_CATEGORY(NTRIPHttpTransportLog, "GPS.NTRIPHttpTransport") + +NTRIPHttpTransport::NTRIPHttpTransport(const NTRIPTransportConfig& config, QObject* parent) + : QObject(parent) + , _hostAddress(config.host) + , _port(config.port) + , _username(config.username) + , _password(config.password) + , _mountpoint(config.mountpoint) + , _useTls(config.useTls) +{ + for (const auto& msg : config.whitelist.split(',')) { + int msg_int = msg.toInt(); + if (msg_int) + _whitelist.append(msg_int); + } + qCDebug(NTRIPHttpTransportLog) << "RTCM message filter:" << _whitelist; + if (_whitelist.empty()) { + qCDebug(NTRIPHttpTransportLog) << "Message filter empty; all RTCM message IDs will be forwarded."; + } + + _connectTimeoutTimer = new QTimer(this); + _connectTimeoutTimer->setSingleShot(true); + connect(_connectTimeoutTimer, &QTimer::timeout, this, [this]() { + qCWarning(NTRIPHttpTransportLog) << "Connection timeout"; + emit error(QStringLiteral("Connection timeout")); + }); + + _dataWatchdogTimer = new QTimer(this); + _dataWatchdogTimer->setSingleShot(true); + _dataWatchdogTimer->setInterval(kDataWatchdogMs); + connect(_dataWatchdogTimer, &QTimer::timeout, this, [this]() { + qCWarning(NTRIPHttpTransportLog) << "No data received for" << kDataWatchdogMs / 1000 << "seconds"; + emit error(tr("No data received for %1 seconds").arg(kDataWatchdogMs / 1000)); + }); +} + +NTRIPHttpTransport::~NTRIPHttpTransport() +{ + stop(); +} + +void NTRIPHttpTransport::start() +{ + _stopped = false; + _connect(); +} + +void NTRIPHttpTransport::stop() +{ + _stopped = true; + _connectTimeoutTimer->stop(); + _dataWatchdogTimer->stop(); + + if (_socket) { + _socket->disconnect(this); + _socket->disconnectFromHost(); + _socket->close(); + _socket->deleteLater(); + _socket = nullptr; + } + + emit finished(); +} + +void NTRIPHttpTransport::_sendHttpRequest() +{ + if (!_socket || _stopped) { + return; + } + + if (!_mountpoint.isEmpty()) { + static const QRegularExpression controlChars(QStringLiteral("[\\r\\n\\x00-\\x1f]")); + if (_mountpoint.contains(controlChars)) { + qCWarning(NTRIPHttpTransportLog) << "Mountpoint contains control characters, rejecting"; + emit error(tr("Invalid mountpoint name (contains control characters)")); + return; + } + + qCDebug(NTRIPHttpTransportLog) << "Sending HTTP request"; + QByteArray req; + req += "GET /" + _mountpoint.toUtf8() + " HTTP/1.0\r\n"; + req += "Host: " + _hostAddress.toUtf8() + "\r\n"; + req += "Ntrip-Version: Ntrip/2.0\r\n"; + req += "User-Agent: NTRIP QGroundControl/1.0\r\n"; + + if (!_username.isEmpty() || !_password.isEmpty()) { + const QByteArray authB64 = (_username + ":" + _password).toUtf8().toBase64(); + req += "Authorization: Basic " + authB64 + "\r\n"; + } + + req += "\r\n"; + _socket->write(req); + _socket->flush(); + + qCDebug(NTRIPHttpTransportLog) << "HTTP request sent for mount:" << _mountpoint; + } else { + _httpHandshakeDone = true; + emit connected(); + _dataWatchdogTimer->start(); + } + + qCDebug(NTRIPHttpTransportLog) << "Socket connected" + << "local" << _socket->localAddress().toString() << ":" << _socket->localPort() + << "-> peer" << _socket->peerAddress().toString() << ":" << _socket->peerPort(); +} + +void NTRIPHttpTransport::_connect() +{ + if (_stopped) { + return; + } + + if (_socket) { + qCWarning(NTRIPHttpTransportLog) << "Socket already exists, aborting connect"; + return; + } + + qCDebug(NTRIPHttpTransportLog) << "connectToHost" << _hostAddress << ":" << _port << " mount=" << _mountpoint; + + _httpHandshakeDone = false; + _httpResponseBuf.clear(); + _rtcmParser.reset(); + + if (_useTls) { + QSslSocket* sslSocket = new QSslSocket(this); + _socket = sslSocket; + connect(sslSocket, QOverload&>::of(&QSslSocket::sslErrors), + this, [](const QList& errors) { + for (const QSslError& e : errors) { + qCWarning(NTRIPHttpTransportLog) << "TLS error:" << e.errorString(); + } + }); + } else { + _socket = new QTcpSocket(this); + } + _socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); + _socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); + _socket->setReadBufferSize(0); + + connect(_socket, &QTcpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError code) { + if (_stopped || !_socket) { + return; + } + _connectTimeoutTimer->stop(); + + QString msg = _socket->errorString(); + if (code == QAbstractSocket::RemoteHostClosedError && !_httpHandshakeDone) { + if (!_mountpoint.isEmpty()) { + msg += " (peer closed before HTTP response; check mountpoint and credentials)"; + } + } + + qCWarning(NTRIPHttpTransportLog) << "Socket error code:" << int(code) << " msg:" << msg; + emit error(msg); + }); + + connect(_socket, &QTcpSocket::disconnected, this, [this]() { + if (_stopped || !_socket) { + return; + } + _connectTimeoutTimer->stop(); + + const QByteArray trailing = _socket->readAll(); + QString reason; + if (!trailing.isEmpty()) { + reason = QString::fromUtf8(trailing).trimmed(); + } else { + reason = QStringLiteral("Server disconnected"); + } + + qCWarning(NTRIPHttpTransportLog) << "Disconnected:" + << "reason=" << reason + << "ms_since_200=" << (_postOkTimestampMs > 0 ? QDateTime::currentMSecsSinceEpoch() - _postOkTimestampMs : -1); + emit error(reason); + }); + + connect(_socket, &QTcpSocket::readyRead, this, &NTRIPHttpTransport::_readBytes); + + if (_useTls) { + QSslSocket* sslSocket = qobject_cast(_socket); + connect(sslSocket, &QSslSocket::encrypted, this, [this]() { + _connectTimeoutTimer->stop(); + _sendHttpRequest(); + }); + sslSocket->connectToHostEncrypted(_hostAddress, static_cast(_port)); + } else { + connect(_socket, &QTcpSocket::connected, this, [this]() { + _connectTimeoutTimer->stop(); + _sendHttpRequest(); + }); + _socket->connectToHost(_hostAddress, static_cast(_port)); + } + _connectTimeoutTimer->start(kConnectTimeoutMs); +} + +void NTRIPHttpTransport::_parseRtcm(const QByteArray& buffer) +{ + if (_stopped) { + return; + } + + for (char ch : buffer) { + const uint8_t byte = static_cast(static_cast(ch)); + + if (!_rtcmParser.addByte(byte)) { + continue; + } + + if (!_rtcmParser.validateCrc()) { + qCWarning(NTRIPHttpTransportLog) << "RTCM CRC mismatch, dropping message id" << _rtcmParser.messageId(); + _rtcmParser.reset(); + continue; + } + + constexpr int kRtcmHeaderSize = 3; + const int payload_len = static_cast(_rtcmParser.messageLength()); + QByteArray message(reinterpret_cast(_rtcmParser.message()), + kRtcmHeaderSize + payload_len); + + const uint8_t* crc_ptr = _rtcmParser.crcBytes(); + message.append(reinterpret_cast(crc_ptr), RTCMParser::kCrcSize); + + const uint16_t id = _rtcmParser.messageId(); + + if (_whitelist.empty() || _whitelist.contains(id)) { + qCDebug(NTRIPHttpTransportLog) << "RTCM packet id" << id << "len" << message.length(); + emit RTCMDataUpdate(message); + } else { + qCDebug(NTRIPHttpTransportLog) << "Ignoring RTCM" << id; + } + + _rtcmParser.reset(); + } +} + +void NTRIPHttpTransport::_readBytes() +{ + if (_stopped || !_socket) { + return; + } + + if (!_httpHandshakeDone) { + _httpResponseBuf.append(_socket->readAll()); + if (_httpResponseBuf.isEmpty()) { + return; + } + + const int hdrEnd = _httpResponseBuf.indexOf("\r\n\r\n"); + if (hdrEnd < 0) { + if (_httpResponseBuf.size() > kMaxHttpHeaderSize) { + qCWarning(NTRIPHttpTransportLog) << "HTTP response header too large, dropping"; + _httpResponseBuf.clear(); + emit error(tr("HTTP response header too large")); + } + return; + } + + const QString header = QString::fromUtf8(_httpResponseBuf.left(hdrEnd)); + qCDebug(NTRIPHttpTransportLog) << "HTTP response received:" << header.left(200); + + const QStringList lines = header.split('\n'); + for (const QString& line : lines) { + const HttpStatus status = parseHttpStatusLine(line); + if (!status.valid) { + continue; + } + + if (isHttpSuccess(status.code)) { + qCDebug(NTRIPHttpTransportLog) << "HTTP" << status.code << status.reason; + _postOkTimestampMs = QDateTime::currentMSecsSinceEpoch(); + _httpHandshakeDone = true; + + qCDebug(NTRIPHttpTransportLog) << "HTTP handshake complete"; + emit connected(); + + _dataWatchdogTimer->start(); + + const QByteArray remainingData = _httpResponseBuf.mid(hdrEnd + 4); + _httpResponseBuf.clear(); + + if (!remainingData.isEmpty()) { + qCDebug(NTRIPHttpTransportLog) << "Processing trailing data:" << remainingData.size() << "bytes"; + _parseRtcm(remainingData); + } + return; + } + + const QString body = QString::fromUtf8(_httpResponseBuf.mid(hdrEnd + 4)).trimmed(); + _httpResponseBuf.clear(); + + if (status.code == 401) { + qCWarning(NTRIPHttpTransportLog) << "Authentication failed:" << status.reason; + emit error(tr("Authentication failed (401): check username and password")); + return; + } + + qCWarning(NTRIPHttpTransportLog) << "HTTP error" << status.code << status.reason + << "body:" << body.left(200); + QString msg = status.reason.isEmpty() + ? tr("HTTP %1").arg(status.code) + : tr("HTTP %1: %2").arg(status.code).arg(status.reason); + if (!body.isEmpty()) { + QString cleanBody = body.left(500); + static const QRegularExpression htmlTags(QStringLiteral("<[^>]*>")); + cleanBody.remove(htmlTags); + cleanBody = cleanBody.simplified().left(200); + if (!cleanBody.isEmpty()) { + msg += QStringLiteral(" — ") + cleanBody; + } + } + emit error(msg); + return; + } + + qCWarning(NTRIPHttpTransportLog) << "No HTTP status line found in response. First line:" + << (lines.isEmpty() ? QStringLiteral("(empty)") : lines.first().left(120)); + _httpResponseBuf.clear(); + emit error(tr("Invalid HTTP response from caster")); + return; + } + + QByteArray bytes = _socket->readAll(); + if (!bytes.isEmpty()) { + _dataWatchdogTimer->start(); + qCDebug(NTRIPHttpTransportLog) << "rx bytes:" << bytes.size(); + _parseRtcm(bytes); + } +} + +void NTRIPHttpTransport::sendNMEA(const QByteArray& nmea) +{ + if (_stopped) { + return; + } + if (!_socket || _socket->state() != QAbstractSocket::ConnectedState) { + return; + } + + const QByteArray line = repairNmeaChecksum(nmea); + qCDebug(NTRIPHttpTransportLog) << "Sent NMEA:" << QString::fromUtf8(line.trimmed()); + _socket->write(line); + _socket->flush(); +} + +NTRIPHttpTransport::HttpStatus NTRIPHttpTransport::parseHttpStatusLine(const QString& line) +{ + static const QRegularExpression re(QStringLiteral("^\\S+\\s+(\\d{3})(?:\\s+(.*))?$")); + const QRegularExpressionMatch match = re.match(line.trimmed()); + + if (!match.hasMatch()) { + return HttpStatus{0, {}, false}; + } + + return HttpStatus{ + match.captured(1).toInt(), + match.captured(2).trimmed(), + true + }; +} + +QByteArray NTRIPHttpTransport::repairNmeaChecksum(const QByteArray& sentence) +{ + QByteArray line = sentence; + + if (line.size() >= 5 && line.at(0) == '$') { + int star = line.lastIndexOf('*'); + if (star > 1) { + quint8 calc = 0; + for (int i = 1; i < star; ++i) { + calc ^= static_cast(line.at(i)); + } + + QByteArray calcCks = QByteArray::number(calc, 16) + .rightJustified(2, '0') + .toUpper(); + + bool needsRepair = false; + if (star + 3 > line.size()) { + needsRepair = true; + } else { + QByteArray txCks = line.mid(star + 1, 2).toUpper(); + if (txCks != calcCks) { + needsRepair = true; + } + } + + if (needsRepair) { + line = line.left(star + 1) + calcCks; + } + } else { + quint8 calc = 0; + for (int i = 1; i < line.size(); ++i) { + calc ^= static_cast(line.at(i)); + } + QByteArray calcCks = QByteArray::number(calc, 16) + .rightJustified(2, '0') + .toUpper(); + line.append('*').append(calcCks); + } + } + + if (!line.endsWith("\r\n")) { + line.append("\r\n"); + } + + return line; +} diff --git a/src/GPS/NTRIP/NTRIPHttpTransport.h b/src/GPS/NTRIP/NTRIPHttpTransport.h new file mode 100644 index 000000000000..522d387c9467 --- /dev/null +++ b/src/GPS/NTRIP/NTRIPHttpTransport.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "RTCMParser.h" + +Q_DECLARE_LOGGING_CATEGORY(NTRIPHttpTransportLog) + +struct NTRIPTransportConfig { + QString host; + int port = 2101; + QString username; + QString password; + QString mountpoint; + QString whitelist; + bool useTls = false; + + bool operator==(const NTRIPTransportConfig& other) const { + return host == other.host && port == other.port && + username == other.username && password == other.password && + mountpoint == other.mountpoint && whitelist == other.whitelist && + useTls == other.useTls; + } + bool operator!=(const NTRIPTransportConfig& other) const { return !(*this == other); } +}; + +class NTRIPHttpTransport : public QObject +{ + Q_OBJECT + friend class NTRIPHttpTransportTest; + +public: + static constexpr int kConnectTimeoutMs = 10000; + static constexpr int kDataWatchdogMs = 30000; + static constexpr int kMaxHttpHeaderSize = 32768; + + explicit NTRIPHttpTransport(const NTRIPTransportConfig& config, QObject* parent = nullptr); + ~NTRIPHttpTransport() override; + + void start(); + void stop(); + void sendNMEA(const QByteArray& nmea); + +signals: + void connected(); + void error(const QString& errorMsg); + void RTCMDataUpdate(const QByteArray& message); + void finished(); + +protected: + struct HttpStatus { + int code = 0; + QString reason; + bool valid = false; + }; + + static HttpStatus parseHttpStatusLine(const QString& line); + static QByteArray repairNmeaChecksum(const QByteArray& sentence); + static bool isHttpSuccess(int code) { return code >= 200 && code < 300; } + +private: + void _connect(); + void _sendHttpRequest(); + void _readBytes(); + void _parseRtcm(const QByteArray& buffer); + + QTcpSocket* _socket = nullptr; + QTimer* _connectTimeoutTimer = nullptr; + QTimer* _dataWatchdogTimer = nullptr; + + QString _hostAddress; + int _port; + QString _username; + QString _password; + QString _mountpoint; + QVector _whitelist; + + RTCMParser _rtcmParser; + bool _httpHandshakeDone = false; + bool _useTls = false; + bool _stopped = false; + + qint64 _postOkTimestampMs = 0; + + QByteArray _httpResponseBuf; +}; diff --git a/src/GPS/NTRIP/NTRIPManager.cc b/src/GPS/NTRIP/NTRIPManager.cc new file mode 100644 index 000000000000..081d22cb1236 --- /dev/null +++ b/src/GPS/NTRIP/NTRIPManager.cc @@ -0,0 +1,630 @@ +#include "NTRIPManager.h" +#include "NTRIPHttpTransport.h" +#include "NTRIPSourceTable.h" +#include "NTRIPSettings.h" +#include "QmlObjectListModel.h" +#include "Fact.h" +#include "FactGroup.h" +#include "QGCApplication.h" +#include "QGCLoggingCategory.h" +#include "RTCMMavlink.h" +#include "SettingsManager.h" + +#include "MultiVehicleManager.h" +#include "PositionManager.h" +#include "Vehicle.h" +#include +#include + +#include +#include +#include +#include + +QGC_LOGGING_CATEGORY(NTRIPManagerLog, "GPS.NTRIPManager") + +Q_APPLICATION_STATIC(NTRIPManager, _ntripManagerInstance); + +NTRIPManager* NTRIPManager::instance() +{ + return _ntripManagerInstance(); +} + +NTRIPManager::NTRIPManager(QObject* parent) + : QObject(parent) +{ + qCDebug(NTRIPManagerLog) << "NTRIPManager created"; + + _rtcmMavlink = qgcApp() ? qgcApp()->findChild() : nullptr; + if (!_rtcmMavlink) { + QObject* parentObj = qgcApp() ? static_cast(qgcApp()) : static_cast(this); + _rtcmMavlink = new RTCMMavlink(parentObj); + _rtcmMavlink->setObjectName(QStringLiteral("RTCMMavlink")); + } + + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (settings) { + auto connectSetting = [this](Fact* fact) { + if (fact) { + connect(fact, &Fact::rawValueChanged, this, &NTRIPManager::_onSettingChanged); + } + }; + connectSetting(settings->ntripServerConnectEnabled()); + connectSetting(settings->ntripServerHostAddress()); + connectSetting(settings->ntripServerPort()); + connectSetting(settings->ntripUsername()); + connectSetting(settings->ntripPassword()); + connectSetting(settings->ntripMountpoint()); + connectSetting(settings->ntripWhitelist()); + connectSetting(settings->ntripUseTls()); + connectSetting(settings->ntripUdpForwardEnabled()); + connectSetting(settings->ntripUdpTargetAddress()); + connectSetting(settings->ntripUdpTargetPort()); + } + + // Check initial state (handles connect-on-start) + QTimer::singleShot(0, this, &NTRIPManager::_onSettingChanged); + + _ggaTimer = new QTimer(this); + _ggaTimer->setInterval(5000); + connect(_ggaTimer, &QTimer::timeout, this, &NTRIPManager::_sendGGA); + + _dataRateTimer = new QTimer(this); + _dataRateTimer->setInterval(1000); + connect(_dataRateTimer, &QTimer::timeout, this, [this]() { + const double rate = static_cast(_bytesReceived - _dataRatePrevBytes); + _dataRatePrevBytes = _bytesReceived; + if (!qFuzzyCompare(_dataRateBytesPerSec + 1.0, rate + 1.0)) { + _dataRateBytesPerSec = rate; + emit dataRateChanged(); + } + emit bytesReceivedChanged(); + emit messagesReceivedChanged(); + }); + + connect(qApp, &QCoreApplication::aboutToQuit, this, &NTRIPManager::stopNTRIP, Qt::QueuedConnection); +} + +NTRIPManager::~NTRIPManager() +{ + qCDebug(NTRIPManagerLog) << "NTRIPManager destroyed"; + stopNTRIP(); +} + +void NTRIPManager::_setStatus(ConnectionStatus status, const QString& msg) +{ + bool changed = false; + + if (_connectionStatus != status) { + _connectionStatus = status; + changed = true; + emit connectionStatusChanged(); + } + + if (_statusMessage != msg) { + _statusMessage = msg; + changed = true; + emit statusMessageChanged(); + } + + if (changed) { + qCDebug(NTRIPManagerLog) << "NTRIP status:" << static_cast(status) << msg; + } +} + +NTRIPTransportConfig NTRIPManager::_configFromSettings() const +{ + NTRIPTransportConfig config; + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (!settings) { + return config; + } + + if (settings->ntripServerHostAddress()) config.host = settings->ntripServerHostAddress()->rawValue().toString(); + if (settings->ntripServerPort()) config.port = settings->ntripServerPort()->rawValue().toInt(); + if (settings->ntripUsername()) config.username = settings->ntripUsername()->rawValue().toString(); + if (settings->ntripPassword()) config.password = settings->ntripPassword()->rawValue().toString(); + if (settings->ntripMountpoint()) config.mountpoint = settings->ntripMountpoint()->rawValue().toString(); + if (settings->ntripWhitelist()) config.whitelist = settings->ntripWhitelist()->rawValue().toString(); + if (settings->ntripUseTls()) config.useTls = settings->ntripUseTls()->rawValue().toBool(); + + return config; +} + +void NTRIPManager::startNTRIP() +{ + if (_startStopBusy || _transport) { + if (_startStopBusy) { + qCWarning(NTRIPManagerLog) << "startNTRIP called while start/stop in progress"; + } + return; + } + _startStopBusy = true; + + if (_reconnectTimer) { + _reconnectTimer->stop(); + } + + NTRIPTransportConfig config = _configFromSettings(); + + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (settings && settings->ntripUdpForwardEnabled()) { + _udpForwardEnabled = settings->ntripUdpForwardEnabled()->rawValue().toBool(); + } + + if (_udpForwardEnabled && settings) { + const QString udpAddr = settings->ntripUdpTargetAddress()->rawValue().toString(); + const quint16 udpPort = static_cast(settings->ntripUdpTargetPort()->rawValue().toUInt()); + const QHostAddress parsedAddr(udpAddr); + if (!udpAddr.isEmpty() && !parsedAddr.isNull() && udpPort > 0) { + _udpTargetAddress = parsedAddr; + _udpTargetPort = udpPort; + if (!_udpSocket) { + _udpSocket = new QUdpSocket(this); + } + qCDebug(NTRIPManagerLog) << "NTRIP UDP forwarding enabled:" << udpAddr << ":" << udpPort; + } else { + qCWarning(NTRIPManagerLog) << "NTRIP UDP forwarding enabled but address/port invalid"; + _udpForwardEnabled = false; + } + } + + if (config.host.isEmpty()) { + qCWarning(NTRIPManagerLog) << "NTRIP host address is empty"; + _setStatus(ConnectionStatus::Error, tr("No host address")); + _startStopBusy = false; + return; + } + + if (config.port <= 0 || config.port > 65535) { + qCWarning(NTRIPManagerLog) << "NTRIP port is invalid:" << config.port; + _setStatus(ConnectionStatus::Error, tr("Invalid port")); + _startStopBusy = false; + return; + } + + qCDebug(NTRIPManagerLog) << "startNTRIP: host=" << config.host << " port=" << config.port + << " mount=" << config.mountpoint; + + _setStatus(ConnectionStatus::Connecting, tr("Connecting to %1:%2...").arg(config.host).arg(config.port)); + + _bytesReceived = 0; + _messagesReceived = 0; + _dataRateBytesPerSec = 0.0; + _dataRatePrevBytes = 0; + emit bytesReceivedChanged(); + emit messagesReceivedChanged(); + emit dataRateChanged(); + + _runningConfig = config; + _runningUdpForward = _udpForwardEnabled; + _runningUdpAddr = _udpForwardEnabled ? _udpTargetAddress.toString() : QString(); + _runningUdpPort = _udpForwardEnabled ? _udpTargetPort : 0; + + _transport = new NTRIPHttpTransport(config, this); + + connect(_transport, &NTRIPHttpTransport::error, + this, &NTRIPManager::_tcpError); + + connect(_transport, &NTRIPHttpTransport::connected, this, [this]() { + _reconnectAttempts = 0; + _casterStatus = CasterStatus::CasterConnected; + emit casterStatusChanged(_casterStatus); + + _setStatus(ConnectionStatus::Connected, tr("Connected")); + + if (_ggaTimer && !_ggaTimer->isActive()) { + _ggaTimer->setInterval(1000); + _sendGGA(); + _ggaTimer->start(); + } + _dataRateTimer->start(); + }); + + connect(_transport, &NTRIPHttpTransport::RTCMDataUpdate, this, &NTRIPManager::_rtcmDataReceived); + + _transport->start(); + qCDebug(NTRIPManagerLog) << "NTRIP started"; + + _startStopBusy = false; +} + +void NTRIPManager::stopNTRIP() +{ + if (_startStopBusy) { + qCWarning(NTRIPManagerLog) << "stopNTRIP called while start/stop in progress"; + return; + } + _startStopBusy = true; + + if (_reconnectTimer) { + _reconnectTimer->stop(); + } + + if (_transport) { + disconnect(_transport, &NTRIPHttpTransport::error, this, &NTRIPManager::_tcpError); + _transport->stop(); + _transport->deleteLater(); + _transport = nullptr; + + _runningConfig = {}; + _runningUdpForward = false; + _runningUdpAddr.clear(); + _runningUdpPort = 0; + + _setStatus(ConnectionStatus::Disconnected, tr("Disconnected")); + qCDebug(NTRIPManagerLog) << "NTRIP stopped"; + } + + if (_ggaTimer && _ggaTimer->isActive()) { + _ggaTimer->stop(); + } + _dataRateTimer->stop(); + if (_dataRateBytesPerSec != 0.0) { + _dataRateBytesPerSec = 0.0; + _dataRatePrevBytes = 0; + emit dataRateChanged(); + } + + if (_udpSocket) { + _udpSocket->close(); + delete _udpSocket; + _udpSocket = nullptr; + } + _udpForwardEnabled = false; + + _startStopBusy = false; +} + +void NTRIPManager::_tcpError(const QString& errorMsg) +{ + qCWarning(NTRIPManagerLog) << "NTRIP error:" << errorMsg; + _setStatus(ConnectionStatus::Error, errorMsg); + + if (errorMsg.contains("NO_LOCATION_PROVIDED", Qt::CaseInsensitive)) { + _casterStatus = CasterStatus::CasterNoLocation; + } else { + _casterStatus = CasterStatus::CasterError; + } + emit casterStatusChanged(_casterStatus); + + _scheduleReconnect(); +} + +void NTRIPManager::_scheduleReconnect() +{ + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + bool shouldRun = settings && settings->ntripServerConnectEnabled() && + settings->ntripServerConnectEnabled()->rawValue().toBool(); + if (!shouldRun) { + return; + } + + stopNTRIP(); + + int backoffMs = qMin(kMinReconnectMs * (1 << qMin(_reconnectAttempts, 4)), kMaxReconnectMs); + _reconnectAttempts = qMin(_reconnectAttempts + 1, kMaxReconnectAttempts); + + qCDebug(NTRIPManagerLog) << "NTRIP reconnecting in" << backoffMs << "ms (attempt" << _reconnectAttempts << ")"; + + _setStatus(ConnectionStatus::Reconnecting, tr("Reconnecting in %1s...").arg(backoffMs / 1000)); + + if (!_reconnectTimer) { + _reconnectTimer = new QTimer(this); + _reconnectTimer->setSingleShot(true); + connect(_reconnectTimer, &QTimer::timeout, this, [this]() { + if (!_transport) { + startNTRIP(); + } + }); + } + + _reconnectTimer->start(backoffMs); +} + +void NTRIPManager::_rtcmDataReceived(const QByteArray& data) +{ + _bytesReceived += static_cast(data.size()); + _messagesReceived++; + + qCDebug(NTRIPManagerLog) << "NTRIP Forwarding RTCM to vehicle:" << data.size() << "bytes"; + + if (!_rtcmMavlink && qgcApp()) { + _rtcmMavlink = qgcApp()->findChild(); + } + + if (_rtcmMavlink) { + _rtcmMavlink->RTCMDataUpdate(data); + + if (_connectionStatus != ConnectionStatus::Connected) { + _setStatus(ConnectionStatus::Connected, tr("Connected")); + } + } else { + qCWarning(NTRIPManagerLog) << "RTCMMavlink not ready; dropping" << data.size() << "bytes"; + } + + if (_udpForwardEnabled && _udpSocket && _udpTargetPort > 0) { + const qint64 sent = _udpSocket->writeDatagram(data, _udpTargetAddress, _udpTargetPort); + if (sent < 0) { + qCWarning(NTRIPManagerLog) << "NTRIP UDP forward failed:" << _udpSocket->errorString(); + } + } +} + +QByteArray NTRIPManager::makeGGA(const QGeoCoordinate& coord, double altitude_msl) +{ + const QTime utc = QDateTime::currentDateTimeUtc().time(); + const QString hhmmss = QString("%1%2%3") + .arg(utc.hour(), 2, 10, QChar('0')) + .arg(utc.minute(), 2, 10, QChar('0')) + .arg(utc.second(), 2, 10, QChar('0')); + + auto dmm = [](double deg, bool lat) -> QString { + double a = qFabs(deg); + int d = int(a); + double m = (a - d) * 60.0; + + int m10000 = int(m * 10000.0 + 0.5); + double m_rounded = m10000 / 10000.0; + if (m_rounded >= 60.0) { + m_rounded -= 60.0; + d += 1; + } + + QString mm = QString::number(m_rounded, 'f', 4); + if (m_rounded < 10.0) { + mm.prepend("0"); + } + + if (lat) { + return QString("%1%2").arg(d, 2, 10, QChar('0')).arg(mm); + } else { + return QString("%1%2").arg(d, 3, 10, QChar('0')).arg(mm); + } + }; + + const bool latNorth = coord.latitude() >= 0.0; + const bool lonEast = coord.longitude() >= 0.0; + + const QString latField = dmm(coord.latitude(), true); + const QString lonField = dmm(coord.longitude(), false); + + const QString core = QString("GPGGA,%1,%2,%3,%4,%5,1,12,1.0,%6,M,0.0,M,,") + .arg(hhmmss) + .arg(latField) + .arg(latNorth ? "N" : "S") + .arg(lonField) + .arg(lonEast ? "E" : "W") + .arg(QString::number(altitude_msl, 'f', 1)); + + quint8 cksum = 0; + const QByteArray coreBytes = core.toUtf8(); + for (char ch : coreBytes) { + cksum ^= static_cast(ch); + } + + const QString cks = QString("%1").arg(cksum, 2, 16, QChar('0')).toUpper(); + const QByteArray sentence = QByteArray("$") + coreBytes + QByteArray("*") + cks.toUtf8(); + return sentence; +} + +QPair NTRIPManager::_getBestPosition() const +{ + MultiVehicleManager* mvm = MultiVehicleManager::instance(); + if (mvm) { + if (Vehicle* veh = mvm->activeVehicle()) { + FactGroup* gps = veh->gpsFactGroup(); + if (gps) { + Fact* latF = gps->getFact(QStringLiteral("lat")); + Fact* lonF = gps->getFact(QStringLiteral("lon")); + + if (latF && lonF) { + const double lat = latF->rawValue().toDouble(); + const double lon = lonF->rawValue().toDouble(); + + if (qIsFinite(lat) && qIsFinite(lon) && + !(lat == 0.0 && lon == 0.0) && + qAbs(lat) <= 90.0 && qAbs(lon) <= 180.0) { + + return {QGeoCoordinate(lat, lon, veh->coordinate().altitude()), + QStringLiteral("GPS Raw")}; + } + } + } + + QGeoCoordinate coord = veh->coordinate(); + if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) { + return {coord, QStringLiteral("Vehicle EKF")}; + } + } + } + + QGCPositionManager* posMgr = QGCPositionManager::instance(); + if (posMgr) { + QGeoCoordinate coord = posMgr->gcsPosition(); + if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) { + return {coord, QStringLiteral("GCS Position")}; + } + } + + return {QGeoCoordinate(), QString()}; +} + +void NTRIPManager::_sendGGA() +{ + if (!_transport) { + return; + } + + const auto [coord, srcUsed] = _getBestPosition(); + + if (!coord.isValid()) { + qCDebug(NTRIPManagerLog) << "NTRIP: No valid position, skipping GGA"; + return; + } + + if (_ggaTimer && _ggaTimer->interval() != 5000) { + _ggaTimer->setInterval(5000); + qCDebug(NTRIPManagerLog) << "NTRIP: Position acquired, reducing GGA to 5s"; + } + + double alt_msl = coord.altitude(); + if (!qIsFinite(alt_msl)) { + alt_msl = 0.0; + } + + const QByteArray gga = makeGGA(coord, alt_msl); + _transport->sendNMEA(gga); + + if (!srcUsed.isEmpty() && srcUsed != _ggaSource) { + _ggaSource = srcUsed; + emit ggaSourceChanged(); + } +} + +void NTRIPManager::_onSettingChanged() +{ + if (_startStopBusy) { + return; + } + + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + bool shouldRun = settings && settings->ntripServerConnectEnabled() && + settings->ntripServerConnectEnabled()->rawValue().toBool(); + bool isRunning = (_transport != nullptr); + + if (shouldRun && isRunning && settings) { + NTRIPTransportConfig config = _configFromSettings(); + const bool udpFwd = settings->ntripUdpForwardEnabled()->rawValue().toBool(); + const QString udpAddr = settings->ntripUdpTargetAddress()->rawValue().toString(); + const quint16 udpPort = static_cast(settings->ntripUdpTargetPort()->rawValue().toUInt()); + + if (config != _runningConfig || + udpFwd != _runningUdpForward || udpAddr != _runningUdpAddr || udpPort != _runningUdpPort) { + qCDebug(NTRIPManagerLog) << "NTRIP settings changed while running, restarting"; + stopNTRIP(); + startNTRIP(); + return; + } + } + + const bool reconnectPending = _reconnectTimer && _reconnectTimer->isActive(); + + if (shouldRun && !isRunning && !reconnectPending) { + startNTRIP(); + } else if (!shouldRun) { + if (_reconnectTimer) { + _reconnectTimer->stop(); + } + if (isRunning) { + stopNTRIP(); + } else if (reconnectPending) { + _reconnectAttempts = 0; + _setStatus(ConnectionStatus::Disconnected, tr("Disconnected")); + } + } +} + +QmlObjectListModel* NTRIPManager::mountpointModel() const +{ + if (_sourceTableModel) { + return _sourceTableModel->mountpoints(); + } + return nullptr; +} + +void NTRIPManager::fetchMountpoints() +{ + if (_mountpointFetchStatus == MountpointFetchStatus::FetchInProgress) { + return; + } + + if (_sourceTableModel && _sourceTableModel->count() > 0 && _sourceTableFetchedAtMs > 0) { + const qint64 age = QDateTime::currentMSecsSinceEpoch() - _sourceTableFetchedAtMs; + if (age < kSourceTableCacheTtlMs) { + qCDebug(NTRIPManagerLog) << "Source table cache hit, age:" << age << "ms"; + _mountpointFetchStatus = MountpointFetchStatus::FetchSuccess; + emit mountpointFetchStatusChanged(); + return; + } + } + + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (!settings) { + return; + } + + const QString host = settings->ntripServerHostAddress()->rawValue().toString(); + const int port = settings->ntripServerPort()->rawValue().toInt(); + const QString user = settings->ntripUsername()->rawValue().toString(); + const QString pass = settings->ntripPassword()->rawValue().toString(); + const bool useTls = settings->ntripUseTls() ? settings->ntripUseTls()->rawValue().toBool() : false; + + if (host.isEmpty()) { + _mountpointFetchError = tr("Host address is empty"); + _mountpointFetchStatus = MountpointFetchStatus::FetchError; + emit mountpointFetchErrorChanged(); + emit mountpointFetchStatusChanged(); + return; + } + + if (!_sourceTableModel) { + _sourceTableModel = new NTRIPSourceTableModel(this); + emit mountpointModelChanged(); + } + + if (_sourceTableFetcher) { + _sourceTableFetcher->deleteLater(); + _sourceTableFetcher = nullptr; + } + + _mountpointFetchStatus = MountpointFetchStatus::FetchInProgress; + _mountpointFetchError.clear(); + emit mountpointFetchStatusChanged(); + emit mountpointFetchErrorChanged(); + + _sourceTableFetcher = new NTRIPSourceTableFetcher(host, port, user, pass, useTls, this); + + connect(_sourceTableFetcher, &NTRIPSourceTableFetcher::sourceTableReceived, this, [this](const QString& table) { + _sourceTableModel->parseSourceTable(table); + _sourceTableFetchedAtMs = QDateTime::currentMSecsSinceEpoch(); + + MultiVehicleManager* mvm = MultiVehicleManager::instance(); + if (mvm) { + if (Vehicle* veh = mvm->activeVehicle()) { + QGeoCoordinate coord = veh->coordinate(); + if (coord.isValid()) { + _sourceTableModel->updateDistances(coord); + } + } + } + + _mountpointFetchStatus = MountpointFetchStatus::FetchSuccess; + emit mountpointFetchStatusChanged(); + qCDebug(NTRIPManagerLog) << "NTRIP source table fetched:" << _sourceTableModel->count() << "mountpoints"; + }); + + connect(_sourceTableFetcher, &NTRIPSourceTableFetcher::error, this, [this](const QString& errorMsg) { + _mountpointFetchError = errorMsg; + _mountpointFetchStatus = MountpointFetchStatus::FetchError; + emit mountpointFetchErrorChanged(); + emit mountpointFetchStatusChanged(); + qCWarning(NTRIPManagerLog) << "NTRIP source table fetch error:" << errorMsg; + }); + + connect(_sourceTableFetcher, &NTRIPSourceTableFetcher::finished, this, [this]() { + _sourceTableFetcher->deleteLater(); + _sourceTableFetcher = nullptr; + }); + + _sourceTableFetcher->fetch(); +} + +void NTRIPManager::selectMountpoint(const QString& mountpoint) +{ + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (settings && settings->ntripMountpoint()) { + settings->ntripMountpoint()->setRawValue(mountpoint); + qCDebug(NTRIPManagerLog) << "NTRIP mountpoint selected:" << mountpoint; + } +} diff --git a/src/GPS/NTRIP/NTRIPManager.h b/src/GPS/NTRIP/NTRIPManager.h new file mode 100644 index 000000000000..0ed8bc2b5ffc --- /dev/null +++ b/src/GPS/NTRIP/NTRIPManager.h @@ -0,0 +1,149 @@ +#pragma once + +#include "NTRIPHttpTransport.h" + +#include +#include +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(NTRIPManagerLog) + +class RTCMMavlink; +class NTRIPSettings; +class Vehicle; +class MultiVehicleManager; +class NTRIPSourceTableModel; +class NTRIPSourceTableFetcher; +class QmlObjectListModel; + +class NTRIPManager : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + Q_MOC_INCLUDE("QmlObjectListModel.h") + Q_PROPERTY(ConnectionStatus connectionStatus READ connectionStatus NOTIFY connectionStatusChanged) + Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged) + Q_PROPERTY(CasterStatus casterStatus READ casterStatus NOTIFY casterStatusChanged) + Q_PROPERTY(QString ggaSource READ ggaSource NOTIFY ggaSourceChanged) + Q_PROPERTY(QmlObjectListModel* mountpointModel READ mountpointModel NOTIFY mountpointModelChanged) + Q_PROPERTY(MountpointFetchStatus mountpointFetchStatus READ mountpointFetchStatus NOTIFY mountpointFetchStatusChanged) + Q_PROPERTY(QString mountpointFetchError READ mountpointFetchError NOTIFY mountpointFetchErrorChanged) + Q_PROPERTY(quint64 bytesReceived READ bytesReceived NOTIFY bytesReceivedChanged) + Q_PROPERTY(quint32 messagesReceived READ messagesReceived NOTIFY messagesReceivedChanged) + Q_PROPERTY(double dataRateBytesPerSec READ dataRateBytesPerSec NOTIFY dataRateChanged) + +public: + enum class ConnectionStatus { + Disconnected, + Connecting, + Connected, + Reconnecting, + Error + }; + Q_ENUM(ConnectionStatus) + + enum class CasterStatus { CasterConnected, CasterNoLocation, CasterError }; + Q_ENUM(CasterStatus) + + enum class MountpointFetchStatus { + FetchIdle, + FetchInProgress, + FetchSuccess, + FetchError + }; + Q_ENUM(MountpointFetchStatus) + + static constexpr int kMinReconnectMs = 1000; + static constexpr int kMaxReconnectMs = 30000; + static constexpr int kMaxReconnectAttempts = 100; + static constexpr int kSourceTableCacheTtlMs = 60000; + + explicit NTRIPManager(QObject* parent = nullptr); + ~NTRIPManager() override; + + static NTRIPManager* instance(); + + ConnectionStatus connectionStatus() const { return _connectionStatus; } + QString statusMessage() const { return _statusMessage; } + CasterStatus casterStatus() const { return _casterStatus; } + QString ggaSource() const { return _ggaSource; } + QmlObjectListModel* mountpointModel() const; + MountpointFetchStatus mountpointFetchStatus() const { return _mountpointFetchStatus; } + QString mountpointFetchError() const { return _mountpointFetchError; } + quint64 bytesReceived() const { return _bytesReceived; } + quint32 messagesReceived() const { return _messagesReceived; } + double dataRateBytesPerSec() const { return _dataRateBytesPerSec; } + + Q_INVOKABLE void fetchMountpoints(); + Q_INVOKABLE void selectMountpoint(const QString& mountpoint); + + void startNTRIP(); + void stopNTRIP(); + + static QByteArray makeGGA(const QGeoCoordinate& coord, double altitude_msl); + +signals: + void connectionStatusChanged(); + void statusMessageChanged(); + void casterStatusChanged(CasterStatus status); + void ggaSourceChanged(); + void mountpointFetchStatusChanged(); + void mountpointModelChanged(); + void mountpointFetchErrorChanged(); + void bytesReceivedChanged(); + void messagesReceivedChanged(); + void dataRateChanged(); + +private: + void _tcpError(const QString& errorMsg); + void _rtcmDataReceived(const QByteArray& data); + void _onSettingChanged(); + void _sendGGA(); + void _scheduleReconnect(); + void _setStatus(ConnectionStatus status, const QString& msg = {}); + + NTRIPTransportConfig _configFromSettings() const; + QPair _getBestPosition() const; + + QTimer* _ggaTimer = nullptr; + + ConnectionStatus _connectionStatus = ConnectionStatus::Disconnected; + QString _statusMessage; + QString _ggaSource; + + NTRIPHttpTransport* _transport = nullptr; + RTCMMavlink* _rtcmMavlink = nullptr; + QTimer* _reconnectTimer = nullptr; + int _reconnectAttempts = 0; + bool _startStopBusy = false; + + CasterStatus _casterStatus = CasterStatus::CasterError; + + QUdpSocket* _udpSocket = nullptr; + QHostAddress _udpTargetAddress; + quint16 _udpTargetPort = 0; + bool _udpForwardEnabled = false; + + NTRIPTransportConfig _runningConfig; + bool _runningUdpForward = false; + QString _runningUdpAddr; + quint16 _runningUdpPort = 0; + + NTRIPSourceTableModel* _sourceTableModel = nullptr; + NTRIPSourceTableFetcher* _sourceTableFetcher = nullptr; + MountpointFetchStatus _mountpointFetchStatus = MountpointFetchStatus::FetchIdle; + QString _mountpointFetchError; + + quint64 _bytesReceived = 0; + quint32 _messagesReceived = 0; + double _dataRateBytesPerSec = 0.0; + quint64 _dataRatePrevBytes = 0; + QTimer* _dataRateTimer = nullptr; + + qint64 _sourceTableFetchedAtMs = 0; +}; diff --git a/src/GPS/NTRIP/NTRIPSourceTable.cc b/src/GPS/NTRIP/NTRIPSourceTable.cc new file mode 100644 index 000000000000..9439e50e455b --- /dev/null +++ b/src/GPS/NTRIP/NTRIPSourceTable.cc @@ -0,0 +1,202 @@ +#include "NTRIPSourceTable.h" +#include "QGCLoggingCategory.h" +#include "QGCNetworkHelper.h" +#include "QmlObjectListModel.h" + +#include +#include + +QGC_LOGGING_CATEGORY(NTRIPSourceTableLog, "GPS.NTRIPSourceTable") + +NTRIPMountpointModel* NTRIPMountpointModel::fromSourceTableLine(const QString& line, QObject* parent) +{ + const QStringList fields = line.split(';'); + if (fields.size() < 18 || fields.at(0).trimmed().toUpper() != QStringLiteral("STR")) { + return nullptr; + } + + auto* model = new NTRIPMountpointModel(parent); + model->_mountpoint = fields.at(1).trimmed(); + model->_identifier = fields.at(2).trimmed(); + model->_format = fields.at(3).trimmed(); + model->_formatDetails = fields.at(4).trimmed(); + model->_carrier = fields.at(5).trimmed().toInt(); + model->_navSystem = fields.at(6).trimmed(); + model->_network = fields.at(7).trimmed(); + model->_country = fields.at(8).trimmed(); + model->_latitude = fields.at(9).trimmed().toDouble(); + model->_longitude = fields.at(10).trimmed().toDouble(); + model->_nmea = fields.at(11).trimmed() == QStringLiteral("1"); + model->_solution = fields.at(12).trimmed() == QStringLiteral("1"); + model->_generator = fields.at(13).trimmed(); + model->_compression = fields.at(14).trimmed(); + model->_authentication = fields.at(15).trimmed(); + model->_fee = fields.at(16).trimmed() == QStringLiteral("Y"); + model->_bitrate = fields.at(17).trimmed().toInt(); + + return model; +} + +void NTRIPMountpointModel::updateDistance(const QGeoCoordinate& from) +{ + if (!from.isValid() || (_latitude == 0.0 && _longitude == 0.0)) { + return; + } + + const QGeoCoordinate mountCoord(_latitude, _longitude); + const double dist = from.distanceTo(mountCoord) / 1000.0; + if (qFuzzyCompare(_distanceKm, dist)) { + return; + } + _distanceKm = dist; + emit distanceKmChanged(); +} + +// --------------------------------------------------------------------------- +// NTRIPSourceTableModel +// --------------------------------------------------------------------------- + +NTRIPSourceTableModel::NTRIPSourceTableModel(QObject* parent) + : QObject(parent) + , _mountpoints(new QmlObjectListModel(this)) +{ +} + +int NTRIPSourceTableModel::count() const +{ + return _mountpoints ? _mountpoints->count() : 0; +} + +void NTRIPSourceTableModel::parseSourceTable(const QString& raw) +{ + clear(); + + const QStringList lines = raw.split('\n'); + for (const QString& line : lines) { + const QString trimmed = line.trimmed(); + if (trimmed.isEmpty() || trimmed.startsWith(QStringLiteral("ENDSOURCETABLE"))) { + continue; + } + NTRIPMountpointModel* mp = NTRIPMountpointModel::fromSourceTableLine(trimmed, _mountpoints); + if (mp) { + _mountpoints->append(mp); + } + } + + emit countChanged(); +} + +void NTRIPSourceTableModel::updateDistances(const QGeoCoordinate& from) +{ + if (!_mountpoints) { + return; + } + for (int i = 0; i < _mountpoints->count(); ++i) { + auto* mp = qobject_cast(_mountpoints->get(i)); + if (mp) { + mp->updateDistance(from); + } + } + sortByDistance(); +} + +void NTRIPSourceTableModel::sortByDistance() +{ + if (!_mountpoints || _mountpoints->count() < 2) { + return; + } + + QObjectList sorted = *_mountpoints->objectList(); + std::sort(sorted.begin(), sorted.end(), [](QObject* a, QObject* b) { + auto* ma = qobject_cast(a); + auto* mb = qobject_cast(b); + if (!ma || !mb) return false; + const double da = ma->distanceKm(); + const double db = mb->distanceKm(); + if (da < 0 && db < 0) return false; + if (da < 0) return false; + if (db < 0) return true; + return da < db; + }); + _mountpoints->swapObjectList(sorted); +} + +void NTRIPSourceTableModel::clear() +{ + if (_mountpoints) { + _mountpoints->clearAndDeleteContents(); + emit countChanged(); + } +} + +// --------------------------------------------------------------------------- +// NTRIPSourceTableFetcher +// --------------------------------------------------------------------------- + +NTRIPSourceTableFetcher::NTRIPSourceTableFetcher(const QString& host, int port, + const QString& username, const QString& password, + bool useTls, + QObject* parent) + : QObject(parent) + , _host(host) + , _port(port) + , _username(username) + , _password(password) + , _useTls(useTls) +{ + _networkManager = QGCNetworkHelper::createNetworkManager(this); +} + +void NTRIPSourceTableFetcher::fetch() +{ + QUrl url; + url.setScheme(_useTls ? QStringLiteral("https") : QStringLiteral("http")); + url.setHost(_host); + url.setPort(_port); + url.setPath(QStringLiteral("/")); + + QGCNetworkHelper::RequestConfig config; + config.timeoutMs = kFetchTimeoutMs; + config.userAgent = QStringLiteral("QGC-NTRIP"); + config.http2Allowed = false; + config.cacheEnabled = false; + + QNetworkRequest request = QGCNetworkHelper::createRequest(url, config); + request.setRawHeader("Ntrip-Version", "Ntrip/2.0"); + if (!_username.isEmpty() || !_password.isEmpty()) { + QGCNetworkHelper::setBasicAuth(request, _username, _password); + } + + _reply = _networkManager->get(request); + connect(_reply, &QNetworkReply::finished, this, &NTRIPSourceTableFetcher::_onReplyFinished); +} + +void NTRIPSourceTableFetcher::_onReplyFinished() +{ + if (!_reply) { + emit error(tr("No reply received")); + emit finished(); + return; + } + + const QString body = QString::fromUtf8(_reply->readAll()); + + if (_reply->error() != QNetworkReply::NoError && !body.contains(QStringLiteral("ENDSOURCETABLE"))) { + emit error(QGCNetworkHelper::errorMessage(_reply)); + _reply->deleteLater(); + _reply = nullptr; + emit finished(); + return; + } + _reply->deleteLater(); + _reply = nullptr; + + if (!body.contains(QStringLiteral("ENDSOURCETABLE"))) { + emit error(tr("Response does not contain a valid source table")); + emit finished(); + return; + } + + emit sourceTableReceived(body); + emit finished(); +} diff --git a/src/GPS/NTRIP/NTRIPSourceTable.h b/src/GPS/NTRIP/NTRIPSourceTable.h new file mode 100644 index 000000000000..1804a1dd5849 --- /dev/null +++ b/src/GPS/NTRIP/NTRIPSourceTable.h @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(NTRIPSourceTableLog) + +class QmlObjectListModel; +class QNetworkAccessManager; + +class NTRIPMountpointModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString mountpoint READ mountpoint CONSTANT) + Q_PROPERTY(QString identifier READ identifier CONSTANT) + Q_PROPERTY(QString format READ format CONSTANT) + Q_PROPERTY(QString formatDetails READ formatDetails CONSTANT) + Q_PROPERTY(int carrier READ carrier CONSTANT) + Q_PROPERTY(QString navSystem READ navSystem CONSTANT) + Q_PROPERTY(QString network READ network CONSTANT) + Q_PROPERTY(QString country READ country CONSTANT) + Q_PROPERTY(double latitude READ latitude CONSTANT) + Q_PROPERTY(double longitude READ longitude CONSTANT) + Q_PROPERTY(bool nmea READ nmea CONSTANT) + Q_PROPERTY(bool solution READ solution CONSTANT) + Q_PROPERTY(QString generator READ generator CONSTANT) + Q_PROPERTY(QString compression READ compression CONSTANT) + Q_PROPERTY(QString authentication READ authentication CONSTANT) + Q_PROPERTY(bool fee READ fee CONSTANT) + Q_PROPERTY(int bitrate READ bitrate CONSTANT) + Q_PROPERTY(double distanceKm READ distanceKm NOTIFY distanceKmChanged) + +public: + explicit NTRIPMountpointModel(QObject* parent = nullptr) : QObject(parent) {} + + QString mountpoint() const { return _mountpoint; } + QString identifier() const { return _identifier; } + QString format() const { return _format; } + QString formatDetails() const { return _formatDetails; } + int carrier() const { return _carrier; } + QString navSystem() const { return _navSystem; } + QString network() const { return _network; } + QString country() const { return _country; } + double latitude() const { return _latitude; } + double longitude() const { return _longitude; } + bool nmea() const { return _nmea; } + bool solution() const { return _solution; } + QString generator() const { return _generator; } + QString compression() const { return _compression; } + QString authentication() const { return _authentication; } + bool fee() const { return _fee; } + int bitrate() const { return _bitrate; } + double distanceKm() const { return _distanceKm; } + + static NTRIPMountpointModel* fromSourceTableLine(const QString& line, QObject* parent = nullptr); + void updateDistance(const QGeoCoordinate& from); + +signals: + void distanceKmChanged(); + +private: + QString _mountpoint; + QString _identifier; + QString _format; + QString _formatDetails; + int _carrier = 0; + QString _navSystem; + QString _network; + QString _country; + double _latitude = 0.0; + double _longitude = 0.0; + bool _nmea = false; + bool _solution = false; + QString _generator; + QString _compression; + QString _authentication; + bool _fee = false; + int _bitrate = 0; + double _distanceKm = -1.0; +}; + +// --------------------------------------------------------------------------- +// NTRIPSourceTableModel +// --------------------------------------------------------------------------- + +class NTRIPSourceTableModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(QmlObjectListModel* mountpoints READ mountpoints CONSTANT) + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + explicit NTRIPSourceTableModel(QObject* parent = nullptr); + + QmlObjectListModel* mountpoints() const { return _mountpoints; } + int count() const; + + void parseSourceTable(const QString& raw); + void updateDistances(const QGeoCoordinate& from); + void sortByDistance(); + void clear(); + +signals: + void countChanged(); + +private: + QmlObjectListModel* _mountpoints = nullptr; +}; + +// --------------------------------------------------------------------------- +// NTRIPSourceTableFetcher +// --------------------------------------------------------------------------- + +class NTRIPSourceTableFetcher : public QObject +{ + Q_OBJECT + +public: + static constexpr int kFetchTimeoutMs = 10000; + + NTRIPSourceTableFetcher(const QString& host, int port, + const QString& username, const QString& password, + bool useTls = false, + QObject* parent = nullptr); + ~NTRIPSourceTableFetcher() override = default; + + void fetch(); + +signals: + void sourceTableReceived(const QString& table); + void error(const QString& errorMsg); + void finished(); + +private: + void _onReplyFinished(); + + QNetworkAccessManager* _networkManager = nullptr; + QNetworkReply* _reply = nullptr; + QString _host; + int _port; + QString _username; + QString _password; + bool _useTls = false; +}; diff --git a/src/GPS/NTRIP/RTCMParser.cc b/src/GPS/NTRIP/RTCMParser.cc new file mode 100644 index 000000000000..b0eaef98b1e8 --- /dev/null +++ b/src/GPS/NTRIP/RTCMParser.cc @@ -0,0 +1,97 @@ +#include "RTCMParser.h" + +RTCMParser::RTCMParser() +{ + reset(); +} + +void RTCMParser::reset() +{ + _state = WaitingForPreamble; + _messageLength = 0; + _bytesRead = 0; + _lengthBytesRead = 0; + _crcBytesRead = 0; +} + +bool RTCMParser::addByte(uint8_t byte) +{ + switch (_state) { + case WaitingForPreamble: + if (byte == RTCM3_PREAMBLE) { + _buffer[0] = byte; + _bytesRead = 1; + _state = ReadingLength; + _lengthBytesRead = 0; + } + break; + + case ReadingLength: + _lengthBytes[_lengthBytesRead++] = byte; + _buffer[_bytesRead++] = byte; + if (_lengthBytesRead == 2) { + _messageLength = ((_lengthBytes[0] & 0x03) << 8) | _lengthBytes[1]; + if (_messageLength > 0 && _messageLength <= kMaxPayloadLength) { + _state = ReadingMessage; + } else { + reset(); + } + } + break; + + case ReadingMessage: + if (_bytesRead < kHeaderSize + kMaxPayloadLength) { + _buffer[_bytesRead++] = byte; + } + if (_bytesRead >= _messageLength + kHeaderSize) { + _state = ReadingCRC; + _crcBytesRead = 0; + } + break; + + case ReadingCRC: + _crcBytes[_crcBytesRead++] = byte; + if (_crcBytesRead == kCrcSize) { + return true; + } + break; + } + return false; +} + +uint16_t RTCMParser::messageId() const +{ + if (_messageLength >= 2) { + return ((_buffer[3] << 4) | (_buffer[4] >> 4)) & 0xFFF; + } + return 0; +} + +uint32_t RTCMParser::crc24q(const uint8_t* data, size_t len) +{ + static constexpr uint32_t kPoly = 0x1864CFB; + uint32_t crc = 0; + for (size_t i = 0; i < len; i++) { + crc ^= static_cast(data[i]) << 16; + for (int j = 0; j < 8; j++) { + crc <<= 1; + if (crc & 0x1000000) { + crc ^= kPoly; + } + } + } + return crc & 0xFFFFFF; +} + +bool RTCMParser::validateCrc() const +{ + if (_messageLength == 0 || _bytesRead < kHeaderSize + _messageLength) { + return false; + } + + const uint32_t computed = crc24q(_buffer, kHeaderSize + _messageLength); + const uint32_t received = (static_cast(_crcBytes[0]) << 16) | + (static_cast(_crcBytes[1]) << 8) | + static_cast(_crcBytes[2]); + return computed == received; +} diff --git a/src/GPS/NTRIP/RTCMParser.h b/src/GPS/NTRIP/RTCMParser.h new file mode 100644 index 000000000000..23dd6df05f1e --- /dev/null +++ b/src/GPS/NTRIP/RTCMParser.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +static constexpr uint8_t RTCM3_PREAMBLE = 0xD3; + +class RTCMParser +{ +public: + RTCMParser(); + void reset(); + bool addByte(uint8_t byte); + uint8_t* message() { return _buffer; } + uint16_t messageLength() const { return _messageLength; } + uint16_t messageId() const; + const uint8_t* crcBytes() const { return _crcBytes; } + static constexpr int kCrcSize = 3; + + bool validateCrc() const; + static uint32_t crc24q(const uint8_t* data, size_t len); + +private: + enum State { + WaitingForPreamble, + ReadingLength, + ReadingMessage, + ReadingCRC + }; + + static constexpr uint16_t kMaxPayloadLength = 1023; + static constexpr int kHeaderSize = 3; + + State _state; + uint8_t _buffer[kHeaderSize + kMaxPayloadLength]; + uint16_t _messageLength; + uint16_t _bytesRead; + uint16_t _lengthBytesRead; + uint8_t _lengthBytes[2]; + uint16_t _crcBytesRead; + uint8_t _crcBytes[3]; +}; diff --git a/src/GPS/RTCMMavlink.cc b/src/GPS/RTCMMavlink.cc index 1853608080ca..9ec7c75bdf3e 100644 --- a/src/GPS/RTCMMavlink.cc +++ b/src/GPS/RTCMMavlink.cc @@ -21,9 +21,7 @@ RTCMMavlink::~RTCMMavlink() void RTCMMavlink::RTCMDataUpdate(QByteArrayView data) { -#ifdef QT_DEBUG _calculateBandwith(data.size()); -#endif mavlink_gps_rtcm_data_t gpsRtcmData{}; diff --git a/src/QmlControls/QGroundControlQmlGlobal.cc b/src/QmlControls/QGroundControlQmlGlobal.cc index fa2b517a686f..5f9b7e5ee454 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.cc +++ b/src/QmlControls/QGroundControlQmlGlobal.cc @@ -11,6 +11,7 @@ #include "PositionManager.h" #include "QGCMapEngineManager.h" #include "ADSBVehicleManager.h" +#include "NTRIPManager.h" #include "MissionCommandTree.h" #include "VideoManager.h" #include "MultiVehicleManager.h" @@ -35,6 +36,7 @@ QGroundControlQmlGlobal::QGroundControlQmlGlobal(QObject *parent) : QObject(parent) , _mapEngineManager(QGCMapEngineManager::instance()) , _adsbVehicleManager(ADSBVehicleManager::instance()) + , _ntripManager(NTRIPManager::instance()) , _qgcPositionManager(QGCPositionManager::instance()) , _missionCommandTree(MissionCommandTree::instance()) , _videoManager(VideoManager::instance()) diff --git a/src/QmlControls/QGroundControlQmlGlobal.h b/src/QmlControls/QGroundControlQmlGlobal.h index 490cbe45224e..2c05c8bfbe98 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.h +++ b/src/QmlControls/QGroundControlQmlGlobal.h @@ -19,6 +19,7 @@ class MissionCommandTree; class MultiVehicleManager; class QGCCorePlugin; class QGCMapEngineManager; +class NTRIPManager; class QGCPalette; class QGCPositionManager; class SettingsManager; @@ -26,6 +27,7 @@ class VideoManager; class QmlObjectListModel; Q_MOC_INCLUDE("ADSBVehicleManager.h") +Q_MOC_INCLUDE("NTRIPManager.h") Q_MOC_INCLUDE("FactGroup.h") Q_MOC_INCLUDE("LinkManager.h") Q_MOC_INCLUDE("MissionCommandTree.h") @@ -67,6 +69,7 @@ class QGroundControlQmlGlobal : public QObject Q_PROPERTY(VideoManager* videoManager READ videoManager CONSTANT) Q_PROPERTY(SettingsManager* settingsManager READ settingsManager CONSTANT) Q_PROPERTY(ADSBVehicleManager* adsbVehicleManager READ adsbVehicleManager CONSTANT) + Q_PROPERTY(NTRIPManager* ntripManager READ ntripManager CONSTANT) Q_PROPERTY(QGCCorePlugin* corePlugin READ corePlugin CONSTANT) Q_PROPERTY(MissionCommandTree* missionCommandTree READ missionCommandTree CONSTANT) #ifndef QGC_NO_SERIAL_LINK @@ -172,6 +175,7 @@ class QGroundControlQmlGlobal : public QObject FactGroup* gpsRtkFactGroup () { return _gpsRtkFactGroup; } #endif ADSBVehicleManager* adsbVehicleManager () { return _adsbVehicleManager; } + NTRIPManager* ntripManager () { return _ntripManager; } QmlUnitsConversion* unitsConversion () { return &_unitsConversion; } static QGeoCoordinate flightMapPosition () { return _coord; } static double flightMapZoom () { return _zoom; } @@ -222,6 +226,7 @@ class QGroundControlQmlGlobal : public QObject private: QGCMapEngineManager* _mapEngineManager = nullptr; ADSBVehicleManager* _adsbVehicleManager = nullptr; + NTRIPManager* _ntripManager = nullptr; QGCPositionManager* _qgcPositionManager = nullptr; MissionCommandTree* _missionCommandTree = nullptr; VideoManager* _videoManager = nullptr; diff --git a/src/Settings/NTRIP.SettingsGroup.json b/src/Settings/NTRIP.SettingsGroup.json index 527e19f39bc7..2348a5481c7e 100644 --- a/src/Settings/NTRIP.SettingsGroup.json +++ b/src/Settings/NTRIP.SettingsGroup.json @@ -4,63 +4,76 @@ "QGC.MetaData.Facts": [ { "name": "ntripServerConnectEnabled", - "shortDescription": "Connect to NTRIP server", - "longDescription": "Connect to NTRIP server using specified address/port", + "shortDesc": "Connect to NTRIP server", + "longDesc": "Connect to NTRIP server using specified address/port", "type": "bool", - "defaultValue": false, - "qgcRebootRequired": true + "default": false }, { "name": "ntripServerHostAddress", - "shortDescription": "Host address", + "shortDesc": "Host address", "type": "string", - "defaultValue": "127.0.0.1", - "qgcRebootRequired": true + "default": "" }, { "name": "ntripServerPort", - "shortDescription": "Server port", - "type": "int32", - "defaultValue": 2101, - "qgcRebootRequired": true + "shortDesc": "Server port", + "type": "uint16", + "default": 2101 }, { "name": "ntripUsername", - "shortDescription": "Username", + "shortDesc": "Username", "type": "string", - "defaultValue": "", - "qgcRebootRequired": true + "default": "" }, { "name": "ntripPassword", - "shortDescription": "Password", + "shortDesc": "Password", "type": "string", - "defaultValue": "", - "qgcRebootRequired": true + "default": "" }, { "name": "ntripMountpoint", - "shortDescription": "Mount Point", - "longDescription": "NTRIP Mount Point. Leave blank for RTCM over TCP", + "shortDesc": "Mount Point", + "longDesc": "NTRIP mount point. Leave blank for RTCM over TCP", "type": "string", - "defaultValue": "", - "qgcRebootRequired": true + "default": "" }, { "name": "ntripWhitelist", - "shortDescription": "RTCM Message Whitelist", - "longDescription": "RTCM Messages to send over mavlink. Leave blank for all messages", + "shortDesc": "RTCM Message Filter", + "longDesc": "Comma-separated RTCM message IDs to forward (e.g. 1005,1077,1087). Leave blank for all messages.", "type": "string", - "defaultValue": "", - "qgcRebootRequired": true + "default": "" }, { - "name": "ntripUseSpartn", - "shortDescription": "Use SPARTN pipeline", - "longDescription": "If enabled, connect with TLS (2102) and treat payload as SPARTN instead of RTCM. Requires SPARTN keys handled by the receiver.", + "name": "ntripUseTls", + "shortDesc": "Use TLS encryption", + "longDesc": "Connect using TLS/SSL encryption (required for some SPARTN casters on port 2102)", "type": "bool", - "defaultValue": false, - "qgcRebootRequired": true + "default": false + }, + { + "name": "ntripUdpForwardEnabled", + "shortDesc": "UDP forward RTCM data", + "longDesc": "Forward received RTCM correction data via UDP to the specified address and port", + "type": "bool", + "default": false + }, + { + "name": "ntripUdpTargetAddress", + "shortDesc": "UDP target address", + "longDesc": "IP address to forward RTCM data to via UDP", + "type": "string", + "default": "" + }, + { + "name": "ntripUdpTargetPort", + "shortDesc": "UDP target port", + "longDesc": "Port to forward RTCM data to via UDP", + "type": "uint16", + "default": 0 } ] } diff --git a/src/Settings/NTRIPSettings.cc b/src/Settings/NTRIPSettings.cc index cb3d9048867b..4ca4617359f4 100644 --- a/src/Settings/NTRIPSettings.cc +++ b/src/Settings/NTRIPSettings.cc @@ -1,62 +1,6 @@ #include "NTRIPSettings.h" -NTRIPSettings::NTRIPSettings(QObject* parent) - : SettingsGroup("NTRIP", "NTRIP", parent) -{ - FactMetaData* metaData = new FactMetaData(FactMetaData::valueTypeBool, this); - metaData->setName("ntripServerConnectEnabled"); - metaData->setShortDescription(tr("Enable NTRIP")); - metaData->setRawDefaultValue(false); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripServerHostAddress"); - metaData->setShortDescription(tr("Host Address")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeUint16, this); - metaData->setName("ntripServerPort"); - metaData->setShortDescription(tr("Port")); - metaData->setRawDefaultValue(2101); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripUsername"); - metaData->setShortDescription(tr("Username")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripPassword"); - metaData->setShortDescription(tr("Password")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripMountpoint"); - metaData->setShortDescription(tr("Mount Point")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripWhitelist"); - metaData->setShortDescription(tr("RTCM Message Whitelist")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeBool, this); - metaData->setName("ntripUseSpartn"); - metaData->setShortDescription(tr("Use SPARTN pipeline")); - metaData->setRawDefaultValue(false); - _nameToMetaDataMap[metaData->name()] = metaData; - - // Force ntripServerConnectEnabled to false at every startup, ignoring saved settings - // This ensures NTRIP never auto-starts regardless of previous user state - if (ntripServerConnectEnabled()) { - ntripServerConnectEnabled()->setRawValue(false); - } -} +DECLARE_SETTINGGROUP(NTRIP, "NTRIP") {} DECLARE_SETTINGSFACT(NTRIPSettings, ntripServerConnectEnabled) DECLARE_SETTINGSFACT(NTRIPSettings, ntripServerHostAddress) @@ -65,4 +9,7 @@ DECLARE_SETTINGSFACT(NTRIPSettings, ntripUsername) DECLARE_SETTINGSFACT(NTRIPSettings, ntripPassword) DECLARE_SETTINGSFACT(NTRIPSettings, ntripMountpoint) DECLARE_SETTINGSFACT(NTRIPSettings, ntripWhitelist) -DECLARE_SETTINGSFACT(NTRIPSettings, ntripUseSpartn) +DECLARE_SETTINGSFACT(NTRIPSettings, ntripUseTls) +DECLARE_SETTINGSFACT(NTRIPSettings, ntripUdpForwardEnabled) +DECLARE_SETTINGSFACT(NTRIPSettings, ntripUdpTargetAddress) +DECLARE_SETTINGSFACT(NTRIPSettings, ntripUdpTargetPort) diff --git a/src/Settings/NTRIPSettings.h b/src/Settings/NTRIPSettings.h index d3619c2039fe..8361ed850a7f 100644 --- a/src/Settings/NTRIPSettings.h +++ b/src/Settings/NTRIPSettings.h @@ -18,5 +18,8 @@ class NTRIPSettings : public SettingsGroup DEFINE_SETTINGFACT(ntripPassword) DEFINE_SETTINGFACT(ntripMountpoint) DEFINE_SETTINGFACT(ntripWhitelist) - DEFINE_SETTINGFACT(ntripUseSpartn) + DEFINE_SETTINGFACT(ntripUseTls) + DEFINE_SETTINGFACT(ntripUdpForwardEnabled) + DEFINE_SETTINGFACT(ntripUdpTargetAddress) + DEFINE_SETTINGFACT(ntripUdpTargetPort) }; diff --git a/src/UI/AppSettings/NTRIPSettings.qml b/src/UI/AppSettings/NTRIPSettings.qml index 4062ebbb0e30..dab0e15eea01 100644 --- a/src/UI/AppSettings/NTRIPSettings.qml +++ b/src/UI/AppSettings/NTRIPSettings.qml @@ -4,116 +4,322 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls -import QGroundControl.NTRIP 1.0 SettingsPage { - property var _settingsManager: QGroundControl.settingsManager - property var _ntrip: _settingsManager.ntripSettings - property Fact _enabled: _ntrip.ntripServerConnectEnabled + property var _settingsManager: QGroundControl.settingsManager + property var _ntrip: _settingsManager.ntripSettings + property Fact _enabled: _ntrip.ntripServerConnectEnabled + property var _ntripMgr: QGroundControl.ntripManager + property bool _isConnected: _ntripMgr.connectionStatus === NTRIPManager.Connected + property bool _isConnecting: _ntripMgr.connectionStatus === NTRIPManager.Connecting + property bool _isReconnecting: _ntripMgr.connectionStatus === NTRIPManager.Reconnecting + property bool _isError: _ntripMgr.connectionStatus === NTRIPManager.Error + property bool _isActive: _enabled.rawValue + property bool _hasHost: _ntrip.ntripServerHostAddress.rawValue !== "" + property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 30 SettingsGroupLayout { Layout.fillWidth: true - heading: qsTr("NTRIP / RTK") + heading: qsTr("Connection") visible: _ntrip.visible - FactCheckBoxSlider { + RowLayout { Layout.fillWidth: true - text: _enabled.shortDescription - fact: _enabled - visible: _enabled.visible - } - } + spacing: ScreenTools.defaultFontPixelWidth - SettingsGroupLayout { - Layout.fillWidth: true - visible: _ntrip.ntripServerHostAddress.visible || _ntrip.ntripServerPort.visible || - _ntrip.ntripUsername.visible || _ntrip.ntripPassword.visible || - _ntrip.ntripMountpoint.visible || _ntrip.ntripWhitelist.visible || - _ntrip.ntripUseSpartn.visible - enabled: _enabled.rawValue + Rectangle { + width: ScreenTools.defaultFontPixelHeight * 0.75 + height: width + radius: width / 2 + color: { + switch (_ntripMgr.connectionStatus) { + case NTRIPManager.Connected: return qgcPal.colorGreen + case NTRIPManager.Connecting: + case NTRIPManager.Reconnecting: return qgcPal.colorOrange + case NTRIPManager.Error: return qgcPal.colorRed + default: return qgcPal.colorGrey + } + } + Layout.alignment: Qt.AlignVCenter + } + + QGCLabel { + Layout.fillWidth: true + text: _ntripMgr.statusMessage || qsTr("Disconnected") + wrapMode: Text.WordWrap + } + + QGCButton { + text: { + if (_isConnecting) return qsTr("Connecting…") + if (_isReconnecting) return qsTr("Reconnecting…") + if (_isConnected) return qsTr("Disconnect") + return qsTr("Connect") + } + enabled: !_isConnecting && (_isActive || _hasHost) + onClicked: _enabled.rawValue = !_enabled.rawValue + } + } - // Status line QGCLabel { Layout.fillWidth: true - Layout.minimumHeight: 30 - visible: true + visible: _isConnected && _ntripMgr.messagesReceived > 0 text: { - try { - return NTRIPManager ? (NTRIPManager.ntripStatus || "Disconnected") : "NTRIP Manager not available" - } catch (e) { - return "Disconnected" - } + var parts = [qsTr("%1 messages").arg(_ntripMgr.messagesReceived), + formatBytes(_ntripMgr.bytesReceived)] + if (_ntripMgr.dataRateBytesPerSec > 0) + parts.push(formatRate(_ntripMgr.dataRateBytesPerSec)) + if (_ntripMgr.ggaSource) + parts.push(qsTr("GGA: %1").arg(_ntripMgr.ggaSource)) + return parts.join(" · ") } - wrapMode: Text.WordWrap - color: { - try { - if (!NTRIPManager) return qgcPal.text - var status = NTRIPManager.ntripStatus || "" - if (status.toLowerCase().includes("connected")) return qgcPal.colorGreen - if (status.toLowerCase().includes("connecting")) return qgcPal.colorOrange - if (status.toLowerCase().includes("error") || status.toLowerCase().includes("failed")) return qgcPal.colorRed - return qgcPal.text - } catch (e) { - return qgcPal.text - } + font.pointSize: ScreenTools.smallFontPointSize + color: qgcPal.colorGrey + + function formatBytes(bytes) { + if (bytes < 1024) return bytes + " B" + if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB" + return (bytes / 1048576).toFixed(1) + " MB" + } + + function formatRate(bytesPerSec) { + if (bytesPerSec < 1024) return bytesPerSec.toFixed(0) + " B/s" + return (bytesPerSec / 1024).toFixed(1) + " KB/s" } } - LabelledFactTextField { + QGCLabel { Layout.fillWidth: true - label: _ntrip.ntripServerHostAddress.shortDescription + visible: _isError && _ntripMgr.statusMessage !== "" + text: _ntripMgr.statusMessage + wrapMode: Text.WordWrap + color: qgcPal.colorRed + font.pointSize: ScreenTools.smallFontPointSize + } + } + + SettingsGroupLayout { + Layout.fillWidth: true + heading: qsTr("Server") + visible: _ntrip.ntripServerHostAddress.visible || _ntrip.ntripServerPort.visible || + _ntrip.ntripUsername.visible || _ntrip.ntripPassword.visible + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth fact: _ntrip.ntripServerHostAddress - visible: _ntrip.ntripServerHostAddress.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 60 + visible: fact.visible + enabled: !_isActive } LabelledFactTextField { - Layout.fillWidth: true - label: _ntrip.ntripServerPort.shortDescription + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth fact: _ntrip.ntripServerPort - visible: _ntrip.ntripServerPort.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 20 + visible: fact.visible + enabled: !_isActive } LabelledFactTextField { - Layout.fillWidth: true - label: _ntrip.ntripUsername.shortDescription + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription fact: _ntrip.ntripUsername - visible: _ntrip.ntripUsername.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 60 + visible: fact.visible + enabled: !_isActive } - LabelledFactTextField { + RowLayout { Layout.fillWidth: true - label: _ntrip.ntripPassword.shortDescription - fact: _ntrip.ntripPassword visible: _ntrip.ntripPassword.visible - textField.echoMode: TextInput.Password - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 60 + spacing: ScreenTools.defaultFontPixelWidth * 0.5 + + LabelledFactTextField { + id: passwordField + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription + fact: _ntrip.ntripPassword + textField.echoMode: _showPassword ? TextInput.Normal : TextInput.Password + enabled: !_isActive + + property bool _showPassword: false + } + + QGCButton { + text: passwordField._showPassword ? qsTr("Hide") : qsTr("Show") + onClicked: passwordField._showPassword = !passwordField._showPassword + Layout.alignment: Qt.AlignBottom + } } - LabelledFactTextField { + FactCheckBoxSlider { Layout.fillWidth: true - label: _ntrip.ntripMountpoint.shortDescription - fact: _ntrip.ntripMountpoint - visible: _ntrip.ntripMountpoint.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 40 + text: fact.shortDescription + fact: _ntrip.ntripUseTls + visible: fact.visible + enabled: !_isActive } + } - LabelledFactTextField { + SettingsGroupLayout { + Layout.fillWidth: true + heading: qsTr("Mountpoint") + visible: _ntrip.ntripMountpoint.visible + + RowLayout { + Layout.fillWidth: true + spacing: ScreenTools.defaultFontPixelWidth + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription + fact: _ntrip.ntripMountpoint + enabled: !_isActive + } + + QGCButton { + text: qsTr("Browse") + enabled: !_isActive && _hasHost && + _ntripMgr.mountpointFetchStatus !== NTRIPManager.FetchInProgress + onClicked: _ntripMgr.fetchMountpoints() + } + } + + QGCLabel { + Layout.fillWidth: true + visible: _ntripMgr.mountpointFetchStatus === NTRIPManager.FetchInProgress + text: qsTr("Fetching mountpoints…") + color: qgcPal.colorOrange + } + + QGCLabel { Layout.fillWidth: true - label: _ntrip.ntripWhitelist.shortDescription + visible: _ntripMgr.mountpointFetchStatus === NTRIPManager.FetchError + text: _ntripMgr.mountpointFetchError + color: qgcPal.colorRed + wrapMode: Text.WordWrap + } + + QGCListView { + Layout.fillWidth: true + Layout.preferredHeight: Math.min(contentHeight, ScreenTools.defaultFontPixelHeight * 20) + visible: _ntripMgr.mountpointModel && _ntripMgr.mountpointModel.count > 0 + model: _ntripMgr.mountpointModel + spacing: ScreenTools.defaultFontPixelHeight * 0.25 + + delegate: Rectangle { + required property int index + required property var object + + width: ListView.view.width + height: mountRow.height + ScreenTools.defaultFontPixelHeight * 0.5 + radius: ScreenTools.defaultFontPixelHeight * 0.25 + color: { + if (object.mountpoint === _ntrip.ntripMountpoint.rawValue) + return qgcPal.buttonHighlight + return index % 2 === 0 ? qgcPal.windowShade : qgcPal.window + } + + RowLayout { + id: mountRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: ScreenTools.defaultFontPixelWidth + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + RowLayout { + spacing: ScreenTools.defaultFontPixelWidth + + QGCLabel { + text: object.mountpoint + font.bold: true + color: object.mountpoint === _ntrip.ntripMountpoint.rawValue + ? qgcPal.buttonHighlightText : qgcPal.text + } + QGCLabel { + visible: object.mountpoint === _ntrip.ntripMountpoint.rawValue + text: qsTr("(selected)") + font.pointSize: ScreenTools.smallFontPointSize + color: qgcPal.buttonHighlightText + } + } + + QGCLabel { + text: { + var parts = [] + if (object.format) parts.push(object.format) + if (object.navSystem) parts.push(object.navSystem) + if (object.country) parts.push(object.country) + if (object.bitrate > 0) parts.push(object.bitrate + " bps") + if (object.distanceKm >= 0) parts.push(object.distanceKm.toFixed(1) + " km") + return parts.join(" · ") + } + font.pointSize: ScreenTools.smallFontPointSize + color: object.mountpoint === _ntrip.ntripMountpoint.rawValue + ? qgcPal.buttonHighlightText : qgcPal.colorGrey + } + } + + QGCButton { + text: object.mountpoint === _ntrip.ntripMountpoint.rawValue + ? qsTr("Selected") : qsTr("Select") + enabled: object.mountpoint !== _ntrip.ntripMountpoint.rawValue + onClicked: _ntripMgr.selectMountpoint(object.mountpoint) + } + } + } + } + } + + SettingsGroupLayout { + Layout.fillWidth: true + heading: qsTr("Options") + visible: _ntrip.ntripWhitelist.visible + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription fact: _ntrip.ntripWhitelist - visible: _ntrip.ntripWhitelist.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 40 + visible: fact.visible + textField.placeholderText: qsTr("e.g. 1005,1077,1087") } + } + + SettingsGroupLayout { + Layout.fillWidth: true + heading: qsTr("UDP Forwarding") + visible: _ntrip.ntripUdpForwardEnabled.visible FactCheckBoxSlider { Layout.fillWidth: true - text: _ntrip.ntripUseSpartn.shortDescription - fact: _ntrip.ntripUseSpartn - visible: _ntrip.ntripUseSpartn.visible - enabled: false + text: fact.shortDescription + fact: _ntrip.ntripUdpForwardEnabled + visible: fact.visible + } + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription + fact: _ntrip.ntripUdpTargetAddress + visible: fact.visible + enabled: _ntrip.ntripUdpForwardEnabled.rawValue + } + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription + fact: _ntrip.ntripUdpTargetPort + visible: fact.visible + enabled: _ntrip.ntripUdpForwardEnabled.rawValue } } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d18cb7d37b6e..046f82d0ae5f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -124,7 +124,10 @@ add_qgc_test(FollowMeTest LABELS Integration Vehicle RESOURCE_LOCK MockLink) # GPS # ---------------------------------------------------------------------------- add_subdirectory(GPS) -add_qgc_test(GpsTest LABELS Unit GPS) +add_qgc_test(RTCMParserTest LABELS Unit) +add_qgc_test(NTRIPManagerTest LABELS Unit) +add_qgc_test(NTRIPHttpTransportTest LABELS Unit) +add_qgc_test(NTRIPSourceTableTest LABELS Unit) # ---------------------------------------------------------------------------- # Joystick diff --git a/test/GPS/CMakeLists.txt b/test/GPS/CMakeLists.txt index 19b3fb5d9416..b2d5c51d74e2 100644 --- a/test/GPS/CMakeLists.txt +++ b/test/GPS/CMakeLists.txt @@ -5,8 +5,14 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE - GpsTest.cc - GpsTest.h + NTRIPManagerTest.cc + NTRIPManagerTest.h + NTRIPSourceTableTest.cc + NTRIPSourceTableTest.h + NTRIPHttpTransportTest.cc + NTRIPHttpTransportTest.h + RTCMParserTest.cc + RTCMParserTest.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/test/GPS/NTRIPHttpTransportTest.cc b/test/GPS/NTRIPHttpTransportTest.cc new file mode 100644 index 000000000000..80c926d1908c --- /dev/null +++ b/test/GPS/NTRIPHttpTransportTest.cc @@ -0,0 +1,303 @@ +#include "NTRIPHttpTransportTest.h" +#include "NTRIPHttpTransport.h" +#include "RTCMParser.h" + +namespace { + +static QByteArray buildRtcmFrame(uint16_t messageId, int extraPayloadBytes = 0) +{ + const int payloadLen = 2 + extraPayloadBytes; + QByteArray frame; + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append(static_cast((payloadLen >> 8) & 0x03)); + frame.append(static_cast(payloadLen & 0xFF)); + frame.append(static_cast((messageId >> 4) & 0xFF)); + frame.append(static_cast((messageId & 0x0F) << 4)); + for (int i = 0; i < extraPayloadBytes; i++) { + frame.append(static_cast(i & 0xFF)); + } + const uint32_t crc = RTCMParser::crc24q( + reinterpret_cast(frame.constData()), + static_cast(frame.size())); + frame.append(static_cast((crc >> 16) & 0xFF)); + frame.append(static_cast((crc >> 8) & 0xFF)); + frame.append(static_cast(crc & 0xFF)); + return frame; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Whitelist Parsing +// --------------------------------------------------------------------------- + +void NTRIPHttpTransportTest::testWhitelistEmpty() +{ + NTRIPHttpTransport t(NTRIPTransportConfig{}); + QVERIFY(t._whitelist.isEmpty()); + + NTRIPTransportConfig cfg; + cfg.whitelist = QStringLiteral(""); + NTRIPHttpTransport t2(cfg); + QVERIFY(t2._whitelist.isEmpty()); +} + +void NTRIPHttpTransportTest::testWhitelistSingle() +{ + NTRIPTransportConfig cfg; + cfg.whitelist = QStringLiteral("1005"); + NTRIPHttpTransport t(cfg); + QCOMPARE(t._whitelist.size(), 1); + QCOMPARE(t._whitelist[0], 1005); +} + +void NTRIPHttpTransportTest::testWhitelistMultiple() +{ + NTRIPTransportConfig cfg; + cfg.whitelist = QStringLiteral("1005,1077,1087"); + NTRIPHttpTransport t(cfg); + QCOMPARE(t._whitelist.size(), 3); + QVERIFY(t._whitelist.contains(1005)); + QVERIFY(t._whitelist.contains(1077)); + QVERIFY(t._whitelist.contains(1087)); +} + +void NTRIPHttpTransportTest::testWhitelistInvalidEntries() +{ + // "abc" and "" should be skipped (toInt returns 0) + NTRIPTransportConfig cfg; + cfg.whitelist = QStringLiteral("1005,abc,,1077"); + NTRIPHttpTransport t(cfg); + QCOMPARE(t._whitelist.size(), 2); + QVERIFY(t._whitelist.contains(1005)); + QVERIFY(t._whitelist.contains(1077)); +} + +// --------------------------------------------------------------------------- +// HTTP Status Line Parsing +// --------------------------------------------------------------------------- + +void NTRIPHttpTransportTest::testParseHttpStatus200() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.1 200 OK"); + QVERIFY(status.valid); + QCOMPARE(status.code, 200); + QCOMPARE(status.reason, QStringLiteral("OK")); +} + +void NTRIPHttpTransportTest::testParseHttpStatusICY() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("ICY 200 OK"); + QVERIFY(status.valid); + QCOMPARE(status.code, 200); + QCOMPARE(status.reason, QStringLiteral("OK")); +} + +void NTRIPHttpTransportTest::testParseHttpStatus401() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.0 401 Unauthorized"); + QVERIFY(status.valid); + QCOMPARE(status.code, 401); + QCOMPARE(status.reason, QStringLiteral("Unauthorized")); +} + +void NTRIPHttpTransportTest::testParseHttpStatus404() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.1 404 Not Found"); + QVERIFY(status.valid); + QCOMPARE(status.code, 404); + QCOMPARE(status.reason, QStringLiteral("Not Found")); +} + +void NTRIPHttpTransportTest::testParseHttpStatusInvalid() +{ + QVERIFY(!NTRIPHttpTransport::parseHttpStatusLine("").valid); + QVERIFY(!NTRIPHttpTransport::parseHttpStatusLine("garbage data").valid); + QVERIFY(!NTRIPHttpTransport::parseHttpStatusLine("200 OK").valid); + + const auto sourceTable = NTRIPHttpTransport::parseHttpStatusLine("SOURCETABLE 200 OK"); + QVERIFY(sourceTable.valid); + QCOMPARE(sourceTable.code, 200); + QCOMPARE(sourceTable.reason, QStringLiteral("OK")); +} + +void NTRIPHttpTransportTest::testParseHttpStatus201() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.1 201 Created"); + QVERIFY(status.valid); + QCOMPARE(status.code, 201); + QCOMPARE(status.reason, QStringLiteral("Created")); +} + +void NTRIPHttpTransportTest::testParseHttpStatus500() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.0 500 Internal Server Error"); + QVERIFY(status.valid); + QCOMPARE(status.code, 500); + QCOMPARE(status.reason, QStringLiteral("Internal Server Error")); +} + +void NTRIPHttpTransportTest::testParseHttpStatusNoReason() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.1 400"); + QVERIFY(status.valid); + QCOMPARE(status.code, 400); + QVERIFY(status.reason.isEmpty()); +} + +// --------------------------------------------------------------------------- +// NMEA Checksum Repair +// --------------------------------------------------------------------------- + +static bool hasValidNmeaChecksum(const QByteArray& sentence) +{ + int star = sentence.lastIndexOf('*'); + if (star < 2 || star + 3 > sentence.size()) { + return false; + } + + quint8 calc = 0; + for (int i = 1; i < star; ++i) { + calc ^= static_cast(sentence.at(i)); + } + + QByteArray expected = QByteArray::number(calc, 16).rightJustified(2, '0').toUpper(); + QByteArray actual = sentence.mid(star + 1, 2).toUpper(); + return actual == expected; +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumCorrect() +{ + const QByteArray input = "$GPGGA,120000,4723.8620,N,00832.7360,E,1,12,1.0,100.0,M,0.0,M,,*64"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + + QVERIFY(result.endsWith("\r\n")); + QVERIFY(hasValidNmeaChecksum(result.trimmed())); + QVERIFY(result.startsWith("$GPGGA,")); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumWrong() +{ + const QByteArray input = "$GPGGA,120000,4723.8620,N,00832.7360,E,1,12,1.0,100.0,M,0.0,M,,*FF"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + + QVERIFY(result.endsWith("\r\n")); + QVERIFY(hasValidNmeaChecksum(result.trimmed())); + QVERIFY(!result.contains("*FF")); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumMissing() +{ + const QByteArray input = "$GPGGA,120000,4723.8620,N,00832.7360,E,1,12,1.0,100.0,M,0.0,M,,"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + + QVERIFY(result.contains("*")); + QVERIFY(result.endsWith("\r\n")); + QVERIFY(hasValidNmeaChecksum(result.trimmed())); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumTruncated() +{ + const QByteArray input = "$GPGGA,120000,0000.0000,N,00000.0000,E,1,12,1.0,0.0,M,0.0,M,,*"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + + QVERIFY(result.endsWith("\r\n")); + const QByteArray trimmed = result.trimmed(); + int star = trimmed.lastIndexOf('*'); + QVERIFY(star > 0); + QVERIFY(star + 3 <= trimmed.size()); + QVERIFY(hasValidNmeaChecksum(trimmed)); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumAppendsCrLf() +{ + const QByteArray input = "$GPGGA,000000,0000.0000,N,00000.0000,E,1,12,1.0,0.0,M,0.0,M,,*00"; + QVERIFY(!input.endsWith("\r\n")); + + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + QVERIFY(result.endsWith("\r\n")); + + const QByteArray input2 = input + "\r\n"; + const QByteArray result2 = NTRIPHttpTransport::repairNmeaChecksum(input2); + QVERIFY(result2.endsWith("\r\n")); + QVERIFY(!result2.endsWith("\r\n\r\n")); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumShortSentence() +{ + const QByteArray input = "$GP"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + QVERIFY(result.endsWith("\r\n")); + + const QByteArray empty = ""; + const QByteArray resultEmpty = NTRIPHttpTransport::repairNmeaChecksum(empty); + QVERIFY(resultEmpty.endsWith("\r\n")); +} + +// --------------------------------------------------------------------------- +// RTCM Filtering +// --------------------------------------------------------------------------- + +void NTRIPHttpTransportTest::testFilterNoWhitelist() +{ + NTRIPTransportConfig cfg; + cfg.mountpoint = QStringLiteral("TEST"); + NTRIPHttpTransport t(cfg); + + QVector received; + connect(&t, &NTRIPHttpTransport::RTCMDataUpdate, this, [&](const QByteArray& msg) { + received.append(msg); + }); + + QByteArray stream = buildRtcmFrame(1005, 4) + buildRtcmFrame(1077, 8) + buildRtcmFrame(1087, 2); + t._parseRtcm(stream); + + QCOMPARE(received.size(), 3); +} + +void NTRIPHttpTransportTest::testFilterWithWhitelist() +{ + NTRIPTransportConfig cfg; + cfg.mountpoint = QStringLiteral("TEST"); + cfg.whitelist = QStringLiteral("1005,1087"); + NTRIPHttpTransport t(cfg); + + QVector receivedIds; + connect(&t, &NTRIPHttpTransport::RTCMDataUpdate, this, [&](const QByteArray& msg) { + if (msg.size() >= 5) { + uint16_t id = (static_cast(msg[3]) << 4) | (static_cast(msg[4]) >> 4); + receivedIds.append(id); + } + }); + + QByteArray stream = buildRtcmFrame(1005, 4) + buildRtcmFrame(1077, 8) + buildRtcmFrame(1087, 2); + t._parseRtcm(stream); + + QCOMPARE(receivedIds.size(), 2); + QVERIFY(receivedIds.contains(1005)); + QVERIFY(receivedIds.contains(1087)); + QVERIFY(!receivedIds.contains(1077)); +} + +void NTRIPHttpTransportTest::testFilterRejectsBadCrc() +{ + NTRIPTransportConfig cfg; + cfg.mountpoint = QStringLiteral("TEST"); + NTRIPHttpTransport t(cfg); + + int count = 0; + connect(&t, &NTRIPHttpTransport::RTCMDataUpdate, this, [&](const QByteArray&) { + count++; + }); + + QByteArray bad = buildRtcmFrame(1005, 4); + bad[bad.size() - 1] = static_cast(bad[bad.size() - 1] ^ 0xFF); + + QByteArray good = buildRtcmFrame(1077, 2); + + t._parseRtcm(bad + good); + + QCOMPARE(count, 1); +} + +UT_REGISTER_TEST(NTRIPHttpTransportTest, TestLabel::Unit) diff --git a/test/GPS/NTRIPHttpTransportTest.h b/test/GPS/NTRIPHttpTransportTest.h new file mode 100644 index 000000000000..09ffdb59240f --- /dev/null +++ b/test/GPS/NTRIPHttpTransportTest.h @@ -0,0 +1,38 @@ +#pragma once + +#include "UnitTest.h" + +class NTRIPHttpTransportTest : public UnitTest +{ + Q_OBJECT + +private slots: + // HTTP status line parsing + void testParseHttpStatus200(); + void testParseHttpStatusICY(); + void testParseHttpStatus401(); + void testParseHttpStatus404(); + void testParseHttpStatusInvalid(); + void testParseHttpStatus201(); + void testParseHttpStatus500(); + void testParseHttpStatusNoReason(); + + // Whitelist parsing + void testWhitelistEmpty(); + void testWhitelistSingle(); + void testWhitelistMultiple(); + void testWhitelistInvalidEntries(); + + // RTCM filtering + void testFilterNoWhitelist(); + void testFilterWithWhitelist(); + void testFilterRejectsBadCrc(); + + // NMEA checksum repair + void testRepairNmeaChecksumCorrect(); + void testRepairNmeaChecksumWrong(); + void testRepairNmeaChecksumMissing(); + void testRepairNmeaChecksumTruncated(); + void testRepairNmeaChecksumAppendsCrLf(); + void testRepairNmeaChecksumShortSentence(); +}; diff --git a/test/GPS/NTRIPManagerTest.cc b/test/GPS/NTRIPManagerTest.cc new file mode 100644 index 000000000000..d66ea0a71927 --- /dev/null +++ b/test/GPS/NTRIPManagerTest.cc @@ -0,0 +1,216 @@ +#include "NTRIPManagerTest.h" +#include "NTRIPManager.h" + +#include + +static bool verifyNmeaChecksum(const QByteArray& sentence) +{ + if (sentence.size() < 6 || sentence.at(0) != '$') { + return false; + } + + int star = sentence.lastIndexOf('*'); + if (star < 2 || star + 3 > sentence.size()) { + return false; + } + + quint8 calc = 0; + for (int i = 1; i < star; ++i) { + calc ^= static_cast(sentence.at(i)); + } + + QByteArray expected = QByteArray::number(calc, 16).rightJustified(2, '0').toUpper(); + QByteArray actual = sentence.mid(star + 1, 2).toUpper(); + return actual == expected; +} + +static int countFields(const QByteArray& gga) +{ + // Count comma-separated fields between $ and * + int star = gga.lastIndexOf('*'); + if (star < 0) star = gga.size(); + QByteArray body = gga.mid(1, star - 1); // strip $ and *XX + return body.count(',') + 1; +} + +// --------------------------------------------------------------------------- +// GGA Format and Structure +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGAFormat() +{ + QGeoCoordinate coord(47.3977, 8.5456); + QByteArray gga = NTRIPManager::makeGGA(coord, 408.0); + + QVERIFY(gga.startsWith("$GPGGA,")); + QVERIFY(gga.contains("*")); + QVERIFY(!gga.contains("\r\n")); + + QVERIFY(gga.contains(",N,")); + QVERIFY(gga.contains(",E,")); + + QVERIFY(gga.contains(",408.0,M,")); +} + +void NTRIPManagerTest::testMakeGGAFieldCount() +{ + // GGA has 15 fields: $GPGGA,hhmmss,lat,N,lon,E,qual,nsat,hdop,alt,M,geoid,M,age,refid + QGeoCoordinate coord(40.0, -74.0); + QByteArray gga = NTRIPManager::makeGGA(coord, 10.0); + + // GPGGA sentence: the body (between $ and *) has 15 comma-separated fields + QCOMPARE(countFields(gga), 15); +} + +void NTRIPManagerTest::testMakeGGAChecksum() +{ + QGeoCoordinate coord(37.7749, -122.4194); + QByteArray gga = NTRIPManager::makeGGA(coord, 16.0); + + QVERIFY(verifyNmeaChecksum(gga)); +} + +// --------------------------------------------------------------------------- +// GGA Hemisphere Encoding +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGANorthEast() +{ + QGeoCoordinate coord(51.5074, 0.1278); + QByteArray gga = NTRIPManager::makeGGA(coord, 11.0); + + QVERIFY(gga.contains(",N,")); + QVERIFY(gga.contains(",E,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGASouthWest() +{ + QGeoCoordinate coord(-33.8688, -151.2093); + QByteArray gga = NTRIPManager::makeGGA(coord, 58.0); + + QVERIFY(gga.contains(",S,")); + QVERIFY(gga.contains(",W,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGAEquator() +{ + // Latitude exactly 0 should be N (>= 0.0) + QGeoCoordinate coord(0.0, 10.0); + QByteArray gga = NTRIPManager::makeGGA(coord, 0.0); + + QVERIFY(gga.contains(",N,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGADateLine() +{ + // Longitude near 180 (Fiji) + QGeoCoordinate coordEast(17.7134, 178.065); + QByteArray ggaEast = NTRIPManager::makeGGA(coordEast, 5.0); + QVERIFY(ggaEast.contains(",E,")); + QVERIFY(verifyNmeaChecksum(ggaEast)); + + // Longitude near -180 (across the date line) + QGeoCoordinate coordWest(17.7134, -179.5); + QByteArray ggaWest = NTRIPManager::makeGGA(coordWest, 5.0); + QVERIFY(ggaWest.contains(",W,")); + QVERIFY(verifyNmeaChecksum(ggaWest)); +} + +// --------------------------------------------------------------------------- +// GGA Altitude +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGAZeroAltitude() +{ + QGeoCoordinate coord(0.0001, 0.0001); + QByteArray gga = NTRIPManager::makeGGA(coord, 0.0); + + QVERIFY(gga.contains(",0.0,M,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGAHighAltitude() +{ + QGeoCoordinate coord(27.9881, 86.9250); + QByteArray gga = NTRIPManager::makeGGA(coord, 8848.9); + + QVERIFY(gga.contains(",8848.9,M,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGANegativeAltitude() +{ + // Dead Sea: ~-430m + QGeoCoordinate coord(31.5, 35.5); + QByteArray gga = NTRIPManager::makeGGA(coord, -430.5); + + QVERIFY(gga.contains(",-430.5,M,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +// --------------------------------------------------------------------------- +// GGA Coordinate Precision +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGADMMPrecision() +{ + // 47.3977 degrees = 47 degrees, 23.8620 minutes + // 8.5456 degrees = 008 degrees, 32.7360 minutes + QGeoCoordinate coord(47.3977, 8.5456); + QByteArray gga = NTRIPManager::makeGGA(coord, 100.0); + + // Extract lat field: after "GPGGA,HHMMSS," the next field is lat + QString ggaStr = QString::fromUtf8(gga); + QStringList fields = ggaStr.mid(1, ggaStr.indexOf('*') - 1).split(','); + // fields[0] = GPGGA, [1] = time, [2] = lat, [3] = N/S, [4] = lon, [5] = E/W + QVERIFY(fields.size() >= 6); + + // Latitude: 4723.xxxx + QString lat = fields[2]; + QVERIFY2(lat.startsWith("4723"), qPrintable(QString("Lat field: %1").arg(lat))); + QCOMPARE(fields[3], QStringLiteral("N")); + + // Longitude: 00832.xxxx + QString lon = fields[4]; + QVERIFY2(lon.startsWith("00832"), qPrintable(QString("Lon field: %1").arg(lon))); + QCOMPARE(fields[5], QStringLiteral("E")); + + QVERIFY(verifyNmeaChecksum(gga)); +} + +// --------------------------------------------------------------------------- +// GGA Time Field +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGATimeFormat() +{ + QGeoCoordinate coord(40.0, -74.0); + QByteArray gga = NTRIPManager::makeGGA(coord, 10.0); + + // Extract time field (field index 1, after $GPGGA,) + QString ggaStr = QString::fromUtf8(gga); + QStringList fields = ggaStr.mid(1, ggaStr.indexOf('*') - 1).split(','); + QVERIFY(fields.size() >= 2); + + const QString timeField = fields[1]; + // Should be HHMMSS.SS format (8 or more chars: 6 digits + dot + decimals) + QVERIFY2(timeField.size() >= 6, qPrintable(QString("Time field too short: %1").arg(timeField))); + // First 6 chars should be digits + for (int i = 0; i < 6; ++i) { + QVERIFY2(timeField[i].isDigit(), qPrintable(QString("Non-digit at pos %1: %2").arg(i).arg(timeField))); + } + // HH should be 00-23 + int hh = timeField.left(2).toInt(); + QVERIFY2(hh >= 0 && hh <= 23, qPrintable(QString("Invalid hour: %1").arg(hh))); + // MM should be 00-59 + int mm = timeField.mid(2, 2).toInt(); + QVERIFY2(mm >= 0 && mm <= 59, qPrintable(QString("Invalid minute: %1").arg(mm))); + // SS should be 00-59 + int ss = timeField.mid(4, 2).toInt(); + QVERIFY2(ss >= 0 && ss <= 59, qPrintable(QString("Invalid second: %1").arg(ss))); +} + +UT_REGISTER_TEST(NTRIPManagerTest, TestLabel::Unit) diff --git a/test/GPS/NTRIPManagerTest.h b/test/GPS/NTRIPManagerTest.h new file mode 100644 index 000000000000..1edbc356f361 --- /dev/null +++ b/test/GPS/NTRIPManagerTest.h @@ -0,0 +1,31 @@ +#pragma once + +#include "UnitTest.h" + +class NTRIPManagerTest : public UnitTest +{ + Q_OBJECT + +private slots: + // GGA format and structure + void testMakeGGAFormat(); + void testMakeGGAFieldCount(); + void testMakeGGAChecksum(); + + // GGA hemisphere encoding + void testMakeGGANorthEast(); + void testMakeGGASouthWest(); + void testMakeGGAEquator(); + void testMakeGGADateLine(); + + // GGA altitude + void testMakeGGAZeroAltitude(); + void testMakeGGAHighAltitude(); + void testMakeGGANegativeAltitude(); + + // GGA coordinate precision + void testMakeGGADMMPrecision(); + + // GGA time field + void testMakeGGATimeFormat(); +}; diff --git a/test/GPS/NTRIPSourceTableTest.cc b/test/GPS/NTRIPSourceTableTest.cc new file mode 100644 index 000000000000..11031379a7d5 --- /dev/null +++ b/test/GPS/NTRIPSourceTableTest.cc @@ -0,0 +1,118 @@ +#include "NTRIPSourceTableTest.h" +#include "NTRIPSourceTable.h" +#include "QmlObjectListModel.h" + +#include + +void NTRIPSourceTableTest::testParseSTRLine() +{ + const QString line = "STR;MOUNT01;Mount Identifier;RTCM 3.2;1005(1),1074(1),1084(1),1094(1);2;GPS+GLO+GAL;NET01;USA;40.0000;-74.0000;0;1;QGC Test;none;B;N;4800;misc"; + auto* mp = NTRIPMountpointModel::fromSourceTableLine(line); + QVERIFY(mp != nullptr); + + QCOMPARE(mp->mountpoint(), QStringLiteral("MOUNT01")); + QCOMPARE(mp->identifier(), QStringLiteral("Mount Identifier")); + QCOMPARE(mp->format(), QStringLiteral("RTCM 3.2")); + QCOMPARE(mp->carrier(), 2); + QCOMPARE(mp->navSystem(), QStringLiteral("GPS+GLO+GAL")); + QCOMPARE(mp->network(), QStringLiteral("NET01")); + QCOMPARE(mp->country(), QStringLiteral("USA")); + QVERIFY(qFuzzyCompare(mp->latitude(), 40.0)); + QVERIFY(qFuzzyCompare(mp->longitude(), -74.0)); + QVERIFY(!mp->nmea()); + QVERIFY(mp->solution()); + QCOMPARE(mp->generator(), QStringLiteral("QGC Test")); + QCOMPARE(mp->authentication(), QStringLiteral("B")); + QVERIFY(!mp->fee()); + QCOMPARE(mp->bitrate(), 4800); + + delete mp; +} + +void NTRIPSourceTableTest::testParseShortLine() +{ + const QString line = "STR;SHORT;Too;Few;Fields"; + auto* mp = NTRIPMountpointModel::fromSourceTableLine(line); + QVERIFY(mp == nullptr); +} + +void NTRIPSourceTableTest::testParseNonSTRLine() +{ + const QString line = "CAS;caster.example.com;2101;NTRIP Caster;Operator;0;USA;0.0;0.0;0.0;0.0;0;0;misc"; + auto* mp = NTRIPMountpointModel::fromSourceTableLine(line); + QVERIFY(mp == nullptr); +} + +void NTRIPSourceTableTest::testParseFullTable() +{ + const QString table = + "SOURCETABLE 200 OK\r\n" + "STR;MP1;Id1;RTCM 3.2;details;2;GPS;NET;USA;40.0;-74.0;0;1;gen;none;B;N;4800;misc\r\n" + "STR;MP2;Id2;RTCM 3.3;details;0;GLO;NET;DEU;52.0;13.0;1;0;gen;none;N;Y;9600;misc\r\n" + "CAS;caster.example.com;2101;desc;op;0;USA;0;0;0;0;0;0;misc\r\n" + "ENDSOURCETABLE\r\n"; + + NTRIPSourceTableModel model; + model.parseSourceTable(table); + + QCOMPARE(model.count(), 2); +} + +void NTRIPSourceTableTest::testDistanceCalculation() +{ + const QString line = "STR;NEAR;Id;RTCM 3.2;details;2;GPS;NET;USA;40.7128;-74.0060;0;1;gen;none;B;N;4800;misc"; + auto* mp = NTRIPMountpointModel::fromSourceTableLine(line); + QVERIFY(mp != nullptr); + + QVERIFY(mp->distanceKm() < 0.0); + + QGeoCoordinate userPos(40.7128, -74.0060); + mp->updateDistance(userPos); + QVERIFY(mp->distanceKm() >= 0.0); + QVERIFY(mp->distanceKm() < 1.0); + + QGeoCoordinate farPos(51.5074, -0.1278); + mp->updateDistance(farPos); + QVERIFY(mp->distanceKm() > 5000.0); + + delete mp; +} + +void NTRIPSourceTableTest::testUpdateDistancesAll() +{ + const QString table = + "STR;NYC;Id1;RTCM 3.2;details;2;GPS;NET;USA;40.7128;-74.0060;0;1;gen;none;B;N;4800;misc\r\n" + "STR;LON;Id2;RTCM 3.3;details;0;GPS;NET;GBR;51.5074;-0.1278;1;0;gen;none;N;Y;9600;misc\r\n" + "ENDSOURCETABLE\r\n"; + + NTRIPSourceTableModel model; + model.parseSourceTable(table); + QCOMPARE(model.count(), 2); + + // Before update, distances should be negative (unset) + auto* mp0 = qobject_cast(model.mountpoints()->get(0)); + auto* mp1 = qobject_cast(model.mountpoints()->get(1)); + QVERIFY(mp0->distanceKm() < 0.0); + QVERIFY(mp1->distanceKm() < 0.0); + + // Update from NYC position + QGeoCoordinate nycPos(40.7128, -74.0060); + model.updateDistances(nycPos); + + // NYC mount should be very close, London should be far + QVERIFY(mp0->distanceKm() >= 0.0); + QVERIFY(mp0->distanceKm() < 1.0); + QVERIFY(mp1->distanceKm() > 5000.0); +} + +void NTRIPSourceTableTest::testEmptyTable() +{ + NTRIPSourceTableModel model; + model.parseSourceTable(""); + QCOMPARE(model.count(), 0); + + model.parseSourceTable("ENDSOURCETABLE\r\n"); + QCOMPARE(model.count(), 0); +} + +UT_REGISTER_TEST(NTRIPSourceTableTest, TestLabel::Unit) diff --git a/test/GPS/NTRIPSourceTableTest.h b/test/GPS/NTRIPSourceTableTest.h new file mode 100644 index 000000000000..110552539ddc --- /dev/null +++ b/test/GPS/NTRIPSourceTableTest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "UnitTest.h" + +class NTRIPSourceTableTest : public UnitTest +{ + Q_OBJECT + +private slots: + void testParseSTRLine(); + void testParseShortLine(); + void testParseNonSTRLine(); + void testParseFullTable(); + void testDistanceCalculation(); + void testUpdateDistancesAll(); + void testEmptyTable(); +}; diff --git a/test/GPS/RTCMParserTest.cc b/test/GPS/RTCMParserTest.cc new file mode 100644 index 000000000000..7d2d8b259524 --- /dev/null +++ b/test/GPS/RTCMParserTest.cc @@ -0,0 +1,351 @@ +#include "RTCMParserTest.h" +#include "RTCMParser.h" + +static QByteArray buildRtcmFrame(uint16_t messageId, int extraPayloadBytes = 0) +{ + const int payloadLen = 2 + extraPayloadBytes; + QByteArray frame; + + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append(static_cast((payloadLen >> 8) & 0x03)); + frame.append(static_cast(payloadLen & 0xFF)); + + frame.append(static_cast((messageId >> 4) & 0xFF)); + frame.append(static_cast((messageId & 0x0F) << 4)); + + for (int i = 0; i < extraPayloadBytes; i++) { + frame.append(static_cast(i & 0xFF)); + } + + const uint32_t crc = RTCMParser::crc24q( + reinterpret_cast(frame.constData()), + static_cast(frame.size())); + + frame.append(static_cast((crc >> 16) & 0xFF)); + frame.append(static_cast((crc >> 8) & 0xFF)); + frame.append(static_cast(crc & 0xFF)); + + return frame; +} + +// --------------------------------------------------------------------------- +// CRC-24Q Tests +// --------------------------------------------------------------------------- + +void RTCMParserTest::testCrc24qEmpty() +{ + const uint32_t crc = RTCMParser::crc24q(nullptr, 0); + QCOMPARE(crc, 0u); +} + +void RTCMParserTest::testCrc24qSingleByte() +{ + // CRC-24Q of 0x00 with initial value 0 is 0 (no bits set to trigger polynomial) + const uint8_t zero = 0x00; + QCOMPARE(RTCMParser::crc24q(&zero, 1), 0u); + + // CRC-24Q of a non-zero byte should produce a non-zero 24-bit result + const uint8_t nonzero = 0x01; + const uint32_t crc = RTCMParser::crc24q(&nonzero, 1); + QVERIFY(crc != 0u); + QCOMPARE(crc & 0xFF000000u, 0u); +} + +void RTCMParserTest::testCrc24qKnownVector() +{ + const uint8_t data[] = { 0xD3, 0x00, 0x02, 0x3E, 0xD0 }; + const uint32_t crc1 = RTCMParser::crc24q(data, sizeof(data)); + const uint32_t crc2 = RTCMParser::crc24q(data, sizeof(data)); + QCOMPARE(crc1, crc2); + QVERIFY((crc1 & 0xFF000000u) == 0u); +} + +void RTCMParserTest::testCrc24qReferenceVector() +{ + // Standard CRC-24Q check value: "123456789" -> 0xCDE703 + const uint8_t data[] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39 }; + const uint32_t crc = RTCMParser::crc24q(data, sizeof(data)); + QCOMPARE(crc, static_cast(0xCDE703)); +} + +void RTCMParserTest::testCrc24qIncremental() +{ + QByteArray frame = buildRtcmFrame(1005, 10); + + // header(3) + payload(12) + crc(3) = 18 + QCOMPARE(frame.size(), 18); + + const uint32_t frameCrc = (static_cast(frame[15]) << 16) | + (static_cast(frame[16]) << 8) | + static_cast(frame[17]); + + const uint32_t computed = RTCMParser::crc24q( + reinterpret_cast(frame.constData()), 15); + + QCOMPARE(computed, frameCrc); +} + +// --------------------------------------------------------------------------- +// RTCMParser State Machine Tests +// --------------------------------------------------------------------------- + +void RTCMParserTest::testParserReset() +{ + RTCMParser parser; + QCOMPARE(parser.messageLength(), static_cast(0)); + QCOMPARE(parser.messageId(), static_cast(0)); + + parser.addByte(RTCM3_PREAMBLE); + parser.addByte(0x00); + parser.reset(); + + QCOMPARE(parser.messageLength(), static_cast(0)); +} + +void RTCMParserTest::testParserValidMessage() +{ + QByteArray frame = buildRtcmFrame(1005, 4); + RTCMParser parser; + + bool complete = false; + for (int i = 0; i < frame.size(); i++) { + complete = parser.addByte(static_cast(frame[i])); + if (i < frame.size() - 1) { + QVERIFY2(!complete, qPrintable(QString("Parser reported complete at byte %1").arg(i))); + } + } + + QVERIFY(complete); + QCOMPARE(parser.messageLength(), static_cast(6)); + QCOMPARE(parser.messageId(), static_cast(1005)); +} + +void RTCMParserTest::testParserCrcValidation() +{ + QByteArray frame = buildRtcmFrame(1077, 8); + RTCMParser parser; + + for (int i = 0; i < frame.size(); i++) { + parser.addByte(static_cast(frame[i])); + } + + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserInvalidCrc() +{ + QByteArray frame = buildRtcmFrame(1077, 8); + frame[frame.size() - 1] = static_cast(frame[frame.size() - 1] ^ 0xFF); + + RTCMParser parser; + for (int i = 0; i < frame.size(); i++) { + parser.addByte(static_cast(frame[i])); + } + + QVERIFY(!parser.validateCrc()); +} + +void RTCMParserTest::testParserMessageId() +{ + const uint16_t testIds[] = { 1001, 1005, 1006, 1033, 1077, 1087, 1097, 1127, 4094 }; + + for (uint16_t id : testIds) { + QByteArray frame = buildRtcmFrame(id, 0); + RTCMParser parser; + + for (int i = 0; i < frame.size(); i++) { + parser.addByte(static_cast(frame[i])); + } + + QCOMPARE(parser.messageId(), id); + } +} + +void RTCMParserTest::testParserGarbageBeforePreamble() +{ + QByteArray frame = buildRtcmFrame(1005, 2); + + QByteArray input; + input.append('\x00'); + input.append('\xFF'); + input.append('\xAA'); + input.append('\x55'); + input.append(frame); + + RTCMParser parser; + bool complete = false; + for (int i = 0; i < input.size(); i++) { + if (parser.addByte(static_cast(input[i]))) { + complete = true; + break; + } + } + + QVERIFY(complete); + QCOMPARE(parser.messageId(), static_cast(1005)); + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserInvalidLength() +{ + // Length = 0 is invalid; parser should reset + QByteArray frame; + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append('\x00'); + frame.append('\x00'); + + RTCMParser parser; + for (int i = 0; i < frame.size(); i++) { + QVERIFY(!parser.addByte(static_cast(frame[i]))); + } + + // Parser should recover and parse a valid message + QByteArray valid = buildRtcmFrame(1005, 0); + bool complete = false; + for (int i = 0; i < valid.size(); i++) { + if (parser.addByte(static_cast(valid[i]))) { + complete = true; + } + } + QVERIFY(complete); + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserOverlengthRejected() +{ + // Length > 1023 (10-bit max) — set reserved bits in length field + QByteArray frame; + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append(static_cast(0x04)); // bit 2 set = 1024 (exceeds 1023) + frame.append(static_cast(0x00)); + + RTCMParser parser; + for (int i = 0; i < frame.size(); i++) { + QVERIFY(!parser.addByte(static_cast(frame[i]))); + } + + // Parser should have reset; verify recovery + QByteArray valid = buildRtcmFrame(1077, 0); + bool complete = false; + for (int i = 0; i < valid.size(); i++) { + if (parser.addByte(static_cast(valid[i]))) { + complete = true; + } + } + QVERIFY(complete); + QCOMPARE(parser.messageId(), static_cast(1077)); +} + +void RTCMParserTest::testParserMultipleMessages() +{ + QByteArray msg1 = buildRtcmFrame(1005, 4); + QByteArray msg2 = buildRtcmFrame(1077, 8); + QByteArray msg3 = buildRtcmFrame(1087, 2); + + QByteArray stream = msg1 + msg2 + msg3; + + RTCMParser parser; + int messageCount = 0; + QVector ids; + + for (int i = 0; i < stream.size(); i++) { + if (parser.addByte(static_cast(stream[i]))) { + QVERIFY(parser.validateCrc()); + ids.append(parser.messageId()); + messageCount++; + parser.reset(); + } + } + + QCOMPARE(messageCount, 3); + QCOMPARE(ids[0], static_cast(1005)); + QCOMPARE(ids[1], static_cast(1077)); + QCOMPARE(ids[2], static_cast(1087)); +} + +void RTCMParserTest::testParserMaxLength() +{ + QByteArray frame; + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append(static_cast(0x03)); // 1023 upper + frame.append(static_cast(0xFF)); // 1023 lower + + // 1023 bytes of payload + frame.append(static_cast(0x3E)); // message ID 1005 upper + frame.append(static_cast(0xD0)); // message ID 1005 lower + for (int i = 0; i < 1021; i++) { + frame.append(static_cast(i & 0xFF)); + } + + const uint32_t crc = RTCMParser::crc24q( + reinterpret_cast(frame.constData()), + static_cast(frame.size())); + frame.append(static_cast((crc >> 16) & 0xFF)); + frame.append(static_cast((crc >> 8) & 0xFF)); + frame.append(static_cast(crc & 0xFF)); + + RTCMParser parser; + bool complete = false; + for (int i = 0; i < frame.size(); i++) { + if (parser.addByte(static_cast(frame[i]))) { + complete = true; + } + } + + QVERIFY(complete); + QCOMPARE(parser.messageLength(), static_cast(1023)); + QCOMPARE(parser.messageId(), static_cast(1005)); + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserTruncatedFrame() +{ + QByteArray frame = buildRtcmFrame(1005, 4); + + // Feed only half the frame; parser should never report complete + RTCMParser parser; + const int halfSize = frame.size() / 2; + for (int i = 0; i < halfSize; i++) { + QVERIFY(!parser.addByte(static_cast(frame[i]))); + } + + // After reset, a full valid frame should still parse + parser.reset(); + bool complete = false; + for (int i = 0; i < frame.size(); i++) { + if (parser.addByte(static_cast(frame[i]))) { + complete = true; + } + } + QVERIFY(complete); + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserCorruptedPreamble() +{ + // Bytes that look like preamble but aren't, followed by a valid frame + QByteArray input; + // Near-miss preamble bytes + input.append(static_cast(0xD2)); + input.append(static_cast(0xD4)); + input.append(static_cast(0x00)); + input.append(static_cast(0x02)); + + QByteArray valid = buildRtcmFrame(1033, 6); + input.append(valid); + + RTCMParser parser; + bool complete = false; + for (int i = 0; i < input.size(); i++) { + if (parser.addByte(static_cast(input[i]))) { + complete = true; + break; + } + } + + QVERIFY(complete); + QCOMPARE(parser.messageId(), static_cast(1033)); + QVERIFY(parser.validateCrc()); +} + +UT_REGISTER_TEST(RTCMParserTest, TestLabel::Unit) diff --git a/test/GPS/RTCMParserTest.h b/test/GPS/RTCMParserTest.h new file mode 100644 index 000000000000..3a92d1244f22 --- /dev/null +++ b/test/GPS/RTCMParserTest.h @@ -0,0 +1,30 @@ +#pragma once + +#include "UnitTest.h" + +class RTCMParserTest : public UnitTest +{ + Q_OBJECT + +private slots: + // CRC-24Q + void testCrc24qEmpty(); + void testCrc24qSingleByte(); + void testCrc24qKnownVector(); + void testCrc24qReferenceVector(); + void testCrc24qIncremental(); + + // RTCMParser state machine + void testParserReset(); + void testParserValidMessage(); + void testParserCrcValidation(); + void testParserInvalidCrc(); + void testParserMessageId(); + void testParserGarbageBeforePreamble(); + void testParserInvalidLength(); + void testParserOverlengthRejected(); + void testParserMultipleMessages(); + void testParserMaxLength(); + void testParserTruncatedFrame(); + void testParserCorruptedPreamble(); +}; diff --git a/tools/simulation/mock_ntrip_caster.py b/tools/simulation/mock_ntrip_caster.py new file mode 100755 index 000000000000..7932a746f3d9 --- /dev/null +++ b/tools/simulation/mock_ntrip_caster.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 +""" +Mock NTRIP Caster for QGroundControl Testing + +Simulates an NTRIP caster that serves a source table and streams RTCM3 +corrections. Useful for testing the QGC NTRIP client without a real caster +or internet connection. + +Usage: + ./mock_ntrip_caster.py # Default: localhost:2101 + ./mock_ntrip_caster.py --port 2102 # Custom port + ./mock_ntrip_caster.py --auth user:pass # Require basic auth + ./mock_ntrip_caster.py --rate 5 # 5 RTCM messages/sec + ./mock_ntrip_caster.py --error bad_crc # Inject bad CRC every 10th msg + ./mock_ntrip_caster.py --error drop # Drop connection after 10 msgs + ./mock_ntrip_caster.py --error http_401 # Always return 401 + ./mock_ntrip_caster.py --error http_500 # Always return 500 + +Real caster for comparison: + Host: rtk2go.com Port: 2101 Password: ntrip@ (or your email) +""" + +import argparse +import base64 +import logging +import select +import socket +import struct +import threading +import time + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger("mock-ntrip") + +# --------------------------------------------------------------------------- +# RTCM3 frame builder +# --------------------------------------------------------------------------- + +# CRC-24Q lookup table (same algorithm as RTCMParser::crc24q) +_CRC24Q_TABLE = [0] * 256 + + +def _init_crc_table(): + for i in range(256): + crc = i << 16 + for _ in range(8): + crc <<= 1 + if crc & 0x1000000: + crc ^= 0x1864CFB + _CRC24Q_TABLE[i] = crc & 0xFFFFFF + + +_init_crc_table() + + +def crc24q(data: bytes) -> int: + crc = 0 + for b in data: + crc = ((crc << 8) & 0xFFFFFF) ^ _CRC24Q_TABLE[((crc >> 16) ^ b) & 0xFF] + return crc + + +def make_rtcm_frame(message_id: int, payload: bytes = b"") -> bytes: + """Build a complete RTCM3 frame with preamble, length, message ID, payload, and CRC.""" + # Message body = 12-bit message ID (big-endian in first 2 bytes) + payload + id_high = (message_id >> 4) & 0xFF + id_low = (message_id & 0x0F) << 4 + if payload: + # Merge low nibble of ID with first nibble of payload + body = bytes([id_high, id_low | (payload[0] >> 4)]) + payload[1:] + else: + body = bytes([id_high, id_low]) + + length = len(body) + if length > 1023: + raise ValueError(f"RTCM payload too large: {length}") + + # Header: preamble (0xD3) + 6 reserved bits (0) + 10-bit length + header = bytes([0xD3, (length >> 8) & 0x03, length & 0xFF]) + + frame_without_crc = header + body + crc = crc24q(frame_without_crc) + crc_bytes = struct.pack(">I", crc)[1:] # 3 bytes big-endian + + return frame_without_crc + crc_bytes + + +def make_rtcm_1005() -> bytes: + """RTCM 1005 - Stationary RTK Reference Station ARP (compact, 19 bytes payload). + Uses a fixed position near 0,0,0 for testing.""" + # Reference Station ID (12 bits) = 1 + # ITRF Realization Year (6 bits) = 0 + # GPS indicator (1) = 1, GLONASS (1) = 0, Galileo (1) = 0, Reference-Station (1) = 0 + # ECEF-X (38 bits signed, 0.0001m) = 0 + # Single Receiver Oscillator (1) = 0, Reserved (1) = 0 + # ECEF-Y (38 bits signed) = 0 + # Quarter Cycle Indicator (2) = 0 + # ECEF-Z (38 bits signed) = 0 + # Total payload: 152 bits = 19 bytes + payload = bytearray(19) + # Station ID = 1 in bits 0..11 -> byte 0 high nibble will be merged with msg ID + payload[0] = 0x00 # station ID high 4 bits (will merge with msg id low nibble) + payload[1] = 0x11 # station ID low 8 bits = 0x01, shifted: 0001 + GPS=1,GLO=0,GAL=0,REF=0 + # Rest is zeros (ECEF 0,0,0) + return make_rtcm_frame(1005, bytes(payload)) + + +def make_rtcm_1077() -> bytes: + """RTCM 1077 - GPS MSM7 (minimal/empty observation for testing).""" + # Minimal MSM header: station ID + epoch + flags, rest zeros + payload = bytearray(25) # minimum MSM7 header size + return make_rtcm_frame(1077, bytes(payload)) + + +def make_bad_crc_frame() -> bytes: + """Build a valid-looking RTCM3 frame with intentionally bad CRC.""" + frame = bytearray(make_rtcm_1005()) + # Corrupt the last CRC byte + frame[-1] ^= 0xFF + return bytes(frame) + + +# --------------------------------------------------------------------------- +# Source table +# --------------------------------------------------------------------------- + +SOURCE_TABLE = """\ +STR;MOCK1;MOCK1;RTCM 3.3;1005(1),1077(1),1087(1);2;GPS+GLO;MOCK;USA;0.00;0.00;0;0;sNTRIP;none;N;N;0; +STR;MOCK2;MOCK2;RTCM 3.3;1005(1),1077(1);2;GPS;MOCK;USA;37.77;-122.42;0;0;sNTRIP;none;N;N;0; +STR;MOCK_AUTH;MOCK_AUTH;RTCM 3.3;1005(1);2;GPS;MOCK;USA;0.00;0.00;0;0;sNTRIP;none;B;N;0; +ENDSOURCETABLE\r\n""" + + +# --------------------------------------------------------------------------- +# Client handler +# --------------------------------------------------------------------------- + + +class ClientHandler(threading.Thread): + def __init__(self, conn, addr, args): + super().__init__(daemon=True) + self.conn = conn + self.addr = addr + self.args = args + self.running = True + + def run(self): + try: + self._handle() + except (ConnectionResetError, BrokenPipeError): + log.info("%s disconnected", self.addr) + except Exception as e: + log.error("%s error: %s", self.addr, e) + finally: + self.conn.close() + + def _handle(self): + # Read HTTP request + buf = b"" + while b"\r\n\r\n" not in buf: + chunk = self.conn.recv(4096) + if not chunk: + return + buf += chunk + if len(buf) > 16384: + log.warning("%s request too large", self.addr) + return + + header_end = buf.index(b"\r\n\r\n") + header = buf[:header_end].decode("utf-8", errors="replace") + lines = header.split("\r\n") + request_line = lines[0] if lines else "" + log.info("%s >> %s", self.addr, request_line) + + # Parse headers + headers = {} + for line in lines[1:]: + if ":" in line: + k, v = line.split(":", 1) + headers[k.strip().lower()] = v.strip() + + # Error injection: HTTP errors + if self.args.error == "http_401": + self._send(b"HTTP/1.1 401 Unauthorized\r\n\r\n") + return + if self.args.error == "http_500": + self._send(b"HTTP/1.1 500 Internal Server Error\r\n\r\n") + return + + # Auth check + if self.args.auth: + expected = base64.b64encode(self.args.auth.encode()).decode() + auth = headers.get("authorization", "") + if not auth or auth.split()[-1] != expected: + log.warning("%s auth failed", self.addr) + self._send(b"HTTP/1.1 401 Unauthorized\r\n\r\n") + return + + # Parse request path + parts = request_line.split() + if len(parts) < 2: + self._send(b"HTTP/1.1 400 Bad Request\r\n\r\n") + return + + path = parts[1] + + if path == "/": + # Source table request + log.info("%s serving source table", self.addr) + body = SOURCE_TABLE.encode() + resp = ( + f"HTTP/1.1 200 OK\r\n" + f"Content-Type: text/plain\r\n" + f"Content-Length: {len(body)}\r\n" + f"\r\n" + ).encode() + body + self._send(resp) + return + + # Mountpoint stream + mountpoint = path.lstrip("/") + valid_mounts = {"MOCK1", "MOCK2", "MOCK_AUTH"} + if mountpoint not in valid_mounts: + log.warning("%s unknown mount: %s", self.addr, mountpoint) + self._send(b"HTTP/1.1 404 Not Found\r\n\r\n") + return + + log.info("%s streaming %s @ %s msg/sec", self.addr, mountpoint, self.args.rate) + # ICY 200 OK (like real NTRIP casters) + self._send(b"ICY 200 OK\r\n\r\n") + + self._stream_rtcm(mountpoint) + + def _stream_rtcm(self, mountpoint): + interval = 1.0 / self.args.rate + msg_count = 0 + + while self.running: + msg_count += 1 + + # Error injection: bad CRC every 10th message + if self.args.error == "bad_crc" and msg_count % 10 == 0: + log.info("%s injecting bad CRC (msg #%d)", self.addr, msg_count) + self._send(make_bad_crc_frame()) + time.sleep(interval) + continue + + # Error injection: drop connection + if self.args.error == "drop" and msg_count > 10: + log.info("%s dropping connection after %d msgs", self.addr, msg_count) + return + + # Alternate between 1005 and 1077 + if msg_count % 2 == 1: + frame = make_rtcm_1005() + else: + frame = make_rtcm_1077() + + try: + self._send(frame) + except (ConnectionResetError, BrokenPipeError): + return + + # Check for incoming NMEA from client (GGA) + try: + ready, _, _ = select.select([self.conn], [], [], 0) + if ready: + nmea = self.conn.recv(4096) + if nmea: + log.info("%s << NMEA: %s", self.addr, nmea.decode("ascii", errors="replace").strip()) + else: + log.info("%s client closed", self.addr) + return + except Exception: + pass + + time.sleep(interval) + + def _send(self, data: bytes): + self.conn.sendall(data) + + +# --------------------------------------------------------------------------- +# Server +# --------------------------------------------------------------------------- + + +def run_server(args): + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind((args.host, args.port)) + srv.listen(5) + + log.info("Mock NTRIP caster listening on %s:%d", args.host, args.port) + log.info("Mountpoints: MOCK1, MOCK2, MOCK_AUTH") + if args.auth: + log.info("Auth required: %s", args.auth) + if args.error: + log.info("Error injection: %s", args.error) + log.info("RTCM rate: %s msg/sec", args.rate) + log.info("") + log.info("QGC settings:") + log.info(" Host: %s", args.host if args.host != "0.0.0.0" else "127.0.0.1") + log.info(" Port: %d", args.port) + log.info(" Mountpoint: MOCK1") + if args.auth: + user, passwd = args.auth.split(":", 1) + log.info(" Username: %s", user) + log.info(" Password: %s", passwd) + log.info("") + + try: + while True: + conn, addr = srv.accept() + log.info("Connection from %s:%d", *addr) + handler = ClientHandler(conn, addr, args) + handler.start() + except KeyboardInterrupt: + log.info("Shutting down") + finally: + srv.close() + + +def main(): + parser = argparse.ArgumentParser( + description="Mock NTRIP caster for QGroundControl testing" + ) + parser.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)") + parser.add_argument("--port", type=int, default=2101, help="Port (default: 2101)") + parser.add_argument("--auth", metavar="USER:PASS", help="Require basic auth (e.g. user:pass)") + parser.add_argument("--rate", type=float, default=1.0, help="Messages per second (default: 1)") + parser.add_argument( + "--error", + choices=["bad_crc", "drop", "http_401", "http_500"], + help="Inject errors: bad_crc (every 10th), drop (after 10), http_401, http_500", + ) + args = parser.parse_args() + run_server(args) + + +if __name__ == "__main__": + main() From 30409882ac8d247673ae5e61a445b6c09a842997 Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Wed, 11 Mar 2026 17:52:32 -0400 Subject: [PATCH 073/119] Refactor GStreamer integration, auto-download SDKs, and CI hardening --- .github/actions/gstreamer/action.yml | 4 + cmake/find-modules/FindQGCGStreamer.cmake | 35 +- cmake/find-modules/GStreamerHelpers.cmake | 67 +- .../VideoReceiver/GStreamer/CMakeLists.txt | 9 +- .../VideoReceiver/GStreamer/GStreamer.cc | 910 ++++++++++-------- .../VideoReceiver/GStreamer/GStreamer.h | 15 +- .../GStreamer/GStreamerHelpers.cc | 279 +++++- .../GStreamer/GStreamerHelpers.h | 14 +- .../GStreamer/GStreamerLogging.cc | 151 +++ .../GStreamer/GStreamerLogging.h | 27 + .../GStreamer/GstVideoReceiver.cc | 139 ++- .../GStreamer/GstVideoReceiver.h | 4 +- .../VideoReceiver/GStreamer/README.md | 162 +--- .../GStreamer/gstqgc/gstqgcvideosinkbin.cc | 129 ++- .../GStreamer/gstqml6gl/CMakeLists.txt | 36 +- .../GStreamer/gstqml6gl/qt6/meson.build | 145 --- tools/README.md | 4 +- tools/common/build_config.py | 10 +- tools/setup/read_config.py | 4 + tools/tests/test_read_config.py | 13 + 20 files changed, 1271 insertions(+), 886 deletions(-) create mode 100644 src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc create mode 100644 src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h delete mode 100644 src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/qt6/meson.build diff --git a/.github/actions/gstreamer/action.yml b/.github/actions/gstreamer/action.yml index 3407645d261d..46224ba4c2e1 100644 --- a/.github/actions/gstreamer/action.yml +++ b/.github/actions/gstreamer/action.yml @@ -258,6 +258,10 @@ runs: $libCount = (Get-ChildItem (Join-Path $prefix 'lib') -Filter '*.lib' | Measure-Object).Count Write-Host "$libCount library files found" + $env:PATH = (Join-Path $prefix 'bin') + ";" + $env:PATH + $env:GST_PLUGIN_PATH = Join-Path $prefix 'lib' 'gstreamer-1.0' + & (Join-Path $prefix 'bin' 'gst-launch-1.0.exe') --version + # ===== ARCHIVE & UPLOAD ===== - name: Configure AWS Credentials (OIDC) diff --git a/cmake/find-modules/FindQGCGStreamer.cmake b/cmake/find-modules/FindQGCGStreamer.cmake index 0d470258302b..2a5b96a1c6f8 100644 --- a/cmake/find-modules/FindQGCGStreamer.cmake +++ b/cmake/find-modules/FindQGCGStreamer.cmake @@ -50,8 +50,6 @@ endif() include(GStreamerHelpers) -# PKG_CONFIG_ARGN is consumed by CMake's FindPkgConfig module (3.22+). -# Extra flags appended here are forwarded to every pkg_check_modules() call. set(PKG_CONFIG_ARGN) function(_qgc_find_apple_pkg_config OUT_VAR) @@ -540,21 +538,26 @@ if(QGCGStreamer_FOUND AND NOT TARGET GStreamer::GStreamer) target_link_directories(GStreamer::GStreamer INTERFACE ${GSTREAMER_LIB_PATH}) - target_link_libraries(GStreamer::GStreamer - INTERFACE - GStreamer::Core - GStreamer::Base - GStreamer::Video - GStreamer::Gl - GStreamer::GlPrototypes - GStreamer::Rtsp - ) + # On macOS with framework, the umbrella library already provides all component + # symbols. Linking individual component dylibs would add @rpath/libgst*.dylib + # load commands that cannot be resolved inside the app bundle. + if(NOT (MACOS AND GStreamer_USE_FRAMEWORK AND GSTREAMER_FRAMEWORK)) + target_link_libraries(GStreamer::GStreamer + INTERFACE + GStreamer::Core + GStreamer::Base + GStreamer::Video + GStreamer::Gl + GStreamer::GlPrototypes + GStreamer::Rtsp + ) - foreach(component IN LISTS QGCGStreamer_FIND_COMPONENTS) - if(GStreamer_${component}_FOUND) - target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::${component}) - endif() - endforeach() + foreach(component IN LISTS QGCGStreamer_FIND_COMPONENTS) + if(GStreamer_${component}_FOUND) + target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::${component}) + endif() + endforeach() + endif() if(GStreamer_USE_STATIC_LIBS) qt_add_library(GStreamer::Plugins INTERFACE IMPORTED) diff --git a/cmake/find-modules/GStreamerHelpers.cmake b/cmake/find-modules/GStreamerHelpers.cmake index d4a7f97c4a06..de472e0f47bd 100644 --- a/cmake/find-modules/GStreamerHelpers.cmake +++ b/cmake/find-modules/GStreamerHelpers.cmake @@ -11,9 +11,19 @@ function(gstreamer_get_package_url PLATFORM VERSION OUTPUT_VAR) elseif(PLATFORM STREQUAL "macos_devel") set(_url "https://gstreamer.freedesktop.org/data/pkg/macos/${VERSION}/gstreamer-1.0-devel-${VERSION}-universal.pkg") elseif(PLATFORM STREQUAL "windows_msvc_x64") - set(_url "https://gstreamer.freedesktop.org/data/pkg/windows/${VERSION}/msvc/gstreamer-1.0-msvc-x86_64-${VERSION}.exe") + if(VERSION VERSION_GREATER_EQUAL "1.28.0") + set(_ext "exe") + else() + set(_ext "msi") + endif() + set(_url "https://gstreamer.freedesktop.org/data/pkg/windows/${VERSION}/msvc/gstreamer-1.0-msvc-x86_64-${VERSION}.${_ext}") elseif(PLATFORM STREQUAL "windows_msvc_arm64") - set(_url "https://gstreamer.freedesktop.org/data/pkg/windows/${VERSION}/msvc/gstreamer-1.0-msvc-arm64-${VERSION}.exe") + if(VERSION VERSION_GREATER_EQUAL "1.28.0") + set(_ext "exe") + else() + set(_ext "msi") + endif() + set(_url "https://gstreamer.freedesktop.org/data/pkg/windows/${VERSION}/msvc/gstreamer-1.0-msvc-arm64-${VERSION}.${_ext}") elseif(PLATFORM STREQUAL "good_plugins") set(_url "https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-${VERSION}.tar.xz") elseif(PLATFORM STREQUAL "good_plugins_qt6") @@ -55,7 +65,6 @@ endfunction() # gstreamer_get_fallback_checksum( ) # Returns a pinned checksum for known artifacts when sidecar checksum fetch fails. -# This preserves integrity verification during transient checksum endpoint outages. function(gstreamer_get_fallback_checksum PLATFORM VERSION OUTPUT_VAR) set(_checksum "") @@ -75,6 +84,12 @@ function(gstreamer_get_fallback_checksum PLATFORM VERSION OUTPUT_VAR) elseif(PLATFORM STREQUAL "good_plugins") set(_checksum "SHA256=d97700f346fdf9ef5461c035e23ed1ce916ca7a31d6ddad987f774774361db77") endif() + elseif(VERSION STREQUAL "1.22.12") + if(PLATFORM STREQUAL "android") + set(_checksum "SHA256=be92cf477d140c270b480bd8ba0e26b1e01c8db042c46b9e234d87352112e485") + elseif(PLATFORM STREQUAL "windows_msvc_x64") + set(_checksum "SHA256=e5cbc6fb9f40fc2850806163df4b9d92012f967c842dc000a2b254cbcd7901d6") + endif() endif() set(${OUTPUT_VAR} "${_checksum}" PARENT_SCOPE) @@ -83,36 +98,12 @@ endfunction() # gstreamer_fetch_checksum( ) # Downloads the .sha256sum sidecar from freedesktop.org and returns "SHA256=". # Returns empty string on failure (checksum unavailable or unparseable). -function(_gstreamer_is_release_like_config OUTPUT_VAR) - set(_release_like FALSE) - - if(CMAKE_CONFIGURATION_TYPES) - foreach(_cfg IN LISTS CMAKE_CONFIGURATION_TYPES) - if(_cfg MATCHES "^(Release|RelWithDebInfo|MinSizeRel)$") - set(_release_like TRUE) - break() - endif() - endforeach() - elseif(CMAKE_BUILD_TYPE MATCHES "^(Release|RelWithDebInfo|MinSizeRel)$") - set(_release_like TRUE) - endif() - - set(${OUTPUT_VAR} "${_release_like}" PARENT_SCOPE) -endfunction() - function(gstreamer_fetch_checksum PLATFORM VERSION OUTPUT_VAR) gstreamer_get_package_url(${PLATFORM} ${VERSION} _pkg_url) # URLs with query strings (e.g. good_plugins_qt6) have no sidecar checksum string(FIND "${_pkg_url}" "?" _qs_pos) if(NOT _qs_pos EQUAL -1) - _gstreamer_is_release_like_config(_require_checksum) - if(_require_checksum) - message(FATAL_ERROR - "GStreamer: Checksums are required for release-like builds, but ${PLATFORM} ${VERSION} " - "uses a query-string URL without sidecar checksum support.\n" - "Use a source with a pinned EXPECTED_HASH.") - endif() message(STATUS "GStreamer: Skipping checksum for ${PLATFORM} ${VERSION} (URL contains query string)") set(${OUTPUT_VAR} "" PARENT_SCOPE) return() @@ -124,7 +115,6 @@ function(gstreamer_fetch_checksum PLATFORM VERSION OUTPUT_VAR) set(_checksum_dir "${CMAKE_BINARY_DIR}/_deps/checksums") set(_checksum_file "${_checksum_dir}/${_url_hash}.sha256sum") - # Try up to 2 rounds: if a cached file fails to parse, delete and re-download. foreach(_round RANGE 1 2) if(NOT EXISTS "${_checksum_file}") file(MAKE_DIRECTORY "${_checksum_dir}") @@ -153,14 +143,9 @@ function(gstreamer_fetch_checksum PLATFORM VERSION OUTPUT_VAR) set(${OUTPUT_VAR} "${_fallback_hash}" PARENT_SCOPE) return() endif() - _gstreamer_is_release_like_config(_require_checksum) - if(_require_checksum) - message(FATAL_ERROR "GStreamer: Checksum not available for ${PLATFORM} ${VERSION} — " - "integrity verification is required for release builds") - endif() message(WARNING - "GStreamer: Checksum sidecar unavailable for ${PLATFORM} ${VERSION}.\n" - "Add a pinned fallback checksum to GStreamerHelpers.cmake for verified builds.") + "GStreamer: Checksum sidecar unavailable for ${PLATFORM} ${VERSION}; " + "continuing without integrity verification.") set(${OUTPUT_VAR} "" PARENT_SCOPE) return() endif() @@ -168,7 +153,6 @@ function(gstreamer_fetch_checksum PLATFORM VERSION OUTPUT_VAR) endif() file(READ "${_checksum_file}" _content) - # Strip surrounding whitespace and any non-hex prefix (e.g. UTF-8 BOM). string(STRIP "${_content}" _content) string(REGEX MATCH "([0-9a-fA-F]+)" _match "${_content}") if(_match) @@ -197,13 +181,7 @@ function(gstreamer_fetch_checksum PLATFORM VERSION OUTPUT_VAR) return() endif() - _gstreamer_is_release_like_config(_require_checksum) - if(_require_checksum) - message(FATAL_ERROR - "GStreamer: Could not parse checksum for ${PLATFORM} ${VERSION} after retry " - "(invalid sidecar content). URL: ${_checksum_url}") - endif() - message(STATUS "GStreamer: Could not parse checksum for ${PLATFORM} ${VERSION} (invalid content)") + message(WARNING "GStreamer: Could not parse checksum for ${PLATFORM} ${VERSION}; continuing without verification.") set(${OUTPUT_VAR} "" PARENT_SCOPE) endfunction() @@ -400,9 +378,6 @@ function(gstreamer_install_libs) return() endif() - # The GStreamer SDK ships only GStreamer and its transitive dependencies — - # there are no unrelated libraries to exclude. Maintaining a prefix allowlist - # would be fragile and silently break when new dependencies appear. file(GLOB all_libs "${ARG_SOURCE_DIR}/*.${ARG_EXTENSION}") if(all_libs) install(FILES ${all_libs} DESTINATION "${ARG_DEST_DIR}") diff --git a/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt b/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt index 244b3277c556..ec48a788370f 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt +++ b/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt @@ -8,8 +8,6 @@ if(QGC_ENABLE_GST_VIDEOSTREAMING) OPTIONAL_COMPONENTS GlEgl GlWayland GlX11 ) - # Expose GST_PLUGIN__FOUND for each discovered plugin so that - # GStreamer.cc can conditionally register static plugin declarations. if(GStreamer_USE_STATIC_LIBS) foreach(_plugin IN LISTS GSTREAMER_PLUGINS) if(GST_PLUGIN_${_plugin}_FOUND) @@ -33,6 +31,8 @@ if(TARGET gstqml6gl) GStreamer.h GStreamerHelpers.cc GStreamerHelpers.h + GStreamerLogging.cc + GStreamerLogging.h GstVideoReceiver.cc GstVideoReceiver.h ) @@ -82,6 +82,7 @@ if(TARGET gstqml6gl) install(PROGRAMS "${_GST_PTP_HELPER}" DESTINATION "lib/gstreamer1.0/gstreamer-1.0") endif() + elseif(WIN32) gstreamer_install_libs( SOURCE_DIR "${GStreamer_ROOT_DIR}/bin" @@ -109,9 +110,8 @@ if(TARGET gstqml6gl) FILES_MATCHING PATTERN "*.exe" ) + elseif(MACOS) - # Framework install preserves internal structure/symlinks required for codesigning. - # Plugin filtering not applied to avoid breaking the framework bundle. if(GSTREAMER_FRAMEWORK) # Resolve the framework real path before install. Some SDK layouts expose # the framework as a symlink; copying through the symlink can leave an @@ -138,4 +138,5 @@ if(TARGET gstqml6gl) "https://gstreamer.freedesktop.org/download/") endif() endif() + endif() diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc index 8e3c18c98cad..c5a3db933342 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc @@ -1,43 +1,36 @@ #include "GStreamer.h" #include "GStreamerHelpers.h" -#include "AppSettings.h" +#include "GStreamerLogging.h" #include "GstVideoReceiver.h" -#include "QGCLoggingCategory.h" #include "SettingsManager.h" #include "VideoSettings.h" #include -#include +#include +#include +#include +#include #include #include -#include +#include + +#ifdef Q_OS_LINUX +#include +#endif -QGC_LOGGING_CATEGORY(GStreamerLog, "Video.GStreamer") -QGC_LOGGING_CATEGORY(GStreamerDecoderRanksLog, "Video.GStreamerDecoderRanks") -QGC_LOGGING_CATEGORY_ON(GStreamerAPILog, "Video.GStreamerAPI") +#include -// TODO: Clean These up with Macros or CMake G_BEGIN_DECLS -GST_PLUGIN_STATIC_DECLARE(androidmedia); -GST_PLUGIN_STATIC_DECLARE(applemedia); +#ifdef QGC_GST_STATIC_BUILD GST_PLUGIN_STATIC_DECLARE(coreelements); -GST_PLUGIN_STATIC_DECLARE(d3d); -GST_PLUGIN_STATIC_DECLARE(d3d11); -GST_PLUGIN_STATIC_DECLARE(d3d12); -GST_PLUGIN_STATIC_DECLARE(dav1d); -GST_PLUGIN_STATIC_DECLARE(dxva); GST_PLUGIN_STATIC_DECLARE(isomp4); GST_PLUGIN_STATIC_DECLARE(libav); GST_PLUGIN_STATIC_DECLARE(matroska); GST_PLUGIN_STATIC_DECLARE(mpegtsdemux); -GST_PLUGIN_STATIC_DECLARE(msdk); -GST_PLUGIN_STATIC_DECLARE(nvcodec); GST_PLUGIN_STATIC_DECLARE(opengl); GST_PLUGIN_STATIC_DECLARE(openh264); GST_PLUGIN_STATIC_DECLARE(playback); -GST_PLUGIN_STATIC_DECLARE(qml6); -GST_PLUGIN_STATIC_DECLARE(qsv); GST_PLUGIN_STATIC_DECLARE(rtp); GST_PLUGIN_STATIC_DECLARE(rtpmanager); GST_PLUGIN_STATIC_DECLARE(rtsp); @@ -45,510 +38,648 @@ GST_PLUGIN_STATIC_DECLARE(sdpelem); GST_PLUGIN_STATIC_DECLARE(tcp); GST_PLUGIN_STATIC_DECLARE(typefindfunctions); GST_PLUGIN_STATIC_DECLARE(udp); -GST_PLUGIN_STATIC_DECLARE(va); +GST_PLUGIN_STATIC_DECLARE(videoconvertscale); GST_PLUGIN_STATIC_DECLARE(videoparsersbad); GST_PLUGIN_STATIC_DECLARE(vpx); + +#ifdef GST_PLUGIN_androidmedia_FOUND +GST_PLUGIN_STATIC_DECLARE(androidmedia); +#endif +#ifdef GST_PLUGIN_applemedia_FOUND +GST_PLUGIN_STATIC_DECLARE(applemedia); +#endif +#ifdef GST_PLUGIN_d3d_FOUND +GST_PLUGIN_STATIC_DECLARE(d3d); +#endif +#ifdef GST_PLUGIN_d3d11_FOUND +GST_PLUGIN_STATIC_DECLARE(d3d11); +#endif +#ifdef GST_PLUGIN_d3d12_FOUND +GST_PLUGIN_STATIC_DECLARE(d3d12); +#endif +#ifdef GST_PLUGIN_dav1d_FOUND +GST_PLUGIN_STATIC_DECLARE(dav1d); +#endif +#ifdef GST_PLUGIN_dxva_FOUND +GST_PLUGIN_STATIC_DECLARE(dxva); +#endif +#ifdef GST_PLUGIN_nvcodec_FOUND +GST_PLUGIN_STATIC_DECLARE(nvcodec); +#endif +#ifdef GST_PLUGIN_qsv_FOUND +GST_PLUGIN_STATIC_DECLARE(qsv); +#endif +#ifdef GST_PLUGIN_va_FOUND +GST_PLUGIN_STATIC_DECLARE(va); +#endif +#ifdef GST_PLUGIN_vulkan_FOUND GST_PLUGIN_STATIC_DECLARE(vulkan); +#endif +#endif +GST_PLUGIN_STATIC_DECLARE(qml6); GST_PLUGIN_STATIC_DECLARE(qgc); G_END_DECLS namespace GStreamer { +static std::atomic s_envPathsValid{true}; +static QMutex s_envPathsMutex; +static QString s_envPathsError; + void _registerPlugins() { #ifdef QGC_GST_STATIC_BUILD - #ifdef GST_PLUGIN_androidmedia_FOUND - GST_PLUGIN_STATIC_REGISTER(androidmedia); - #endif - #ifdef GST_PLUGIN_applemedia_FOUND - GST_PLUGIN_STATIC_REGISTER(applemedia); - #endif - GST_PLUGIN_STATIC_REGISTER(coreelements); - #ifdef GST_PLUGIN_d3d_FOUND - GST_PLUGIN_STATIC_REGISTER(d3d); - #endif - #ifdef GST_PLUGIN_d3d11_FOUND - GST_PLUGIN_STATIC_REGISTER(d3d11); - #endif - #ifdef GST_PLUGIN_d3d12_FOUND - GST_PLUGIN_STATIC_REGISTER(d3d12); - #endif - #ifdef GST_PLUGIN_dav1d_FOUND - GST_PLUGIN_STATIC_REGISTER(dav1d); - #endif - #ifdef GST_PLUGIN_dxva_FOUND - GST_PLUGIN_STATIC_REGISTER(dxva); - #endif - GST_PLUGIN_STATIC_REGISTER(isomp4); - GST_PLUGIN_STATIC_REGISTER(libav); - GST_PLUGIN_STATIC_REGISTER(matroska); - GST_PLUGIN_STATIC_REGISTER(mpegtsdemux); - #ifdef GST_PLUGIN_msdk_FOUND - GST_PLUGIN_STATIC_REGISTER(msdk); - #endif - #ifdef GST_PLUGIN_nvcodec_FOUND - GST_PLUGIN_STATIC_REGISTER(nvcodec); - #endif - GST_PLUGIN_STATIC_REGISTER(opengl); - GST_PLUGIN_STATIC_REGISTER(openh264); - GST_PLUGIN_STATIC_REGISTER(playback); - #ifdef GST_PLUGIN_qsv_FOUND - GST_PLUGIN_STATIC_REGISTER(qsv); - #endif - GST_PLUGIN_STATIC_REGISTER(rtp); - GST_PLUGIN_STATIC_REGISTER(rtpmanager); - GST_PLUGIN_STATIC_REGISTER(rtsp); - GST_PLUGIN_STATIC_REGISTER(sdpelem); - GST_PLUGIN_STATIC_REGISTER(tcp); - GST_PLUGIN_STATIC_REGISTER(typefindfunctions); - GST_PLUGIN_STATIC_REGISTER(udp); - #ifdef GST_PLUGIN_va_FOUND - GST_PLUGIN_STATIC_REGISTER(va); - #endif - GST_PLUGIN_STATIC_REGISTER(videoparsersbad); - GST_PLUGIN_STATIC_REGISTER(vpx); - #ifdef GST_PLUGIN_vulkan_FOUND - GST_PLUGIN_STATIC_REGISTER(vulkan); - #endif + GST_PLUGIN_STATIC_REGISTER(coreelements); + GST_PLUGIN_STATIC_REGISTER(isomp4); + GST_PLUGIN_STATIC_REGISTER(libav); + GST_PLUGIN_STATIC_REGISTER(matroska); + GST_PLUGIN_STATIC_REGISTER(mpegtsdemux); + GST_PLUGIN_STATIC_REGISTER(opengl); + GST_PLUGIN_STATIC_REGISTER(openh264); + GST_PLUGIN_STATIC_REGISTER(playback); + GST_PLUGIN_STATIC_REGISTER(rtp); + GST_PLUGIN_STATIC_REGISTER(rtpmanager); + GST_PLUGIN_STATIC_REGISTER(rtsp); + GST_PLUGIN_STATIC_REGISTER(sdpelem); + GST_PLUGIN_STATIC_REGISTER(tcp); + GST_PLUGIN_STATIC_REGISTER(typefindfunctions); + GST_PLUGIN_STATIC_REGISTER(udp); + GST_PLUGIN_STATIC_REGISTER(videoconvertscale); + GST_PLUGIN_STATIC_REGISTER(videoparsersbad); + GST_PLUGIN_STATIC_REGISTER(vpx); + +#ifdef GST_PLUGIN_androidmedia_FOUND + GST_PLUGIN_STATIC_REGISTER(androidmedia); +#endif +#ifdef GST_PLUGIN_applemedia_FOUND + GST_PLUGIN_STATIC_REGISTER(applemedia); +#endif +#ifdef GST_PLUGIN_d3d_FOUND + GST_PLUGIN_STATIC_REGISTER(d3d); +#endif +#ifdef GST_PLUGIN_d3d11_FOUND + GST_PLUGIN_STATIC_REGISTER(d3d11); +#endif +#ifdef GST_PLUGIN_d3d12_FOUND + GST_PLUGIN_STATIC_REGISTER(d3d12); +#endif +#ifdef GST_PLUGIN_dav1d_FOUND + GST_PLUGIN_STATIC_REGISTER(dav1d); +#endif +#ifdef GST_PLUGIN_dxva_FOUND + GST_PLUGIN_STATIC_REGISTER(dxva); +#endif +#ifdef GST_PLUGIN_nvcodec_FOUND + GST_PLUGIN_STATIC_REGISTER(nvcodec); +#endif +#ifdef GST_PLUGIN_qsv_FOUND + GST_PLUGIN_STATIC_REGISTER(qsv); +#endif +#ifdef GST_PLUGIN_va_FOUND + GST_PLUGIN_STATIC_REGISTER(va); +#endif +#ifdef GST_PLUGIN_vulkan_FOUND + GST_PLUGIN_STATIC_REGISTER(vulkan); +#endif #endif -// #if !defined(GST_PLUGIN_qml6_FOUND) && defined(QGC_GST_STATIC_BUILD) GST_PLUGIN_STATIC_REGISTER(qml6); -// #endif - GST_PLUGIN_STATIC_REGISTER(qgc); } -void _qtGstLog(GstDebugCategory *category, - GstDebugLevel level, - const gchar *file, - const gchar *function, - gint line, - GObject *object, - GstDebugMessage *message, - gpointer data) +void _resetEnvValidation() { - Q_UNUSED(data); + const QMutexLocker locker(&s_envPathsMutex); + s_envPathsError.clear(); + s_envPathsValid.store(true, std::memory_order_release); +} - if (level > gst_debug_category_get_threshold(category)) { - return; - } +void _setEnvValidationError(const QString &error) +{ + const QMutexLocker locker(&s_envPathsMutex); + s_envPathsError = error; + s_envPathsValid.store(false, std::memory_order_release); + qCCritical(GStreamerLog) << error; +} - QMessageLogger log(file, line, function); - - char *object_info = gst_info_strdup_printf("%" GST_PTR_FORMAT, static_cast(object)); - - switch (level) { - case GST_LEVEL_ERROR: - log.critical(GStreamerAPILog, "%s %s", object_info, gst_debug_message_get(message)); - break; - case GST_LEVEL_WARNING: - log.warning(GStreamerAPILog, "%s %s", object_info, gst_debug_message_get(message)); - break; - case GST_LEVEL_FIXME: - case GST_LEVEL_INFO: - log.info(GStreamerAPILog, "%s %s", object_info, gst_debug_message_get(message)); - break; - case GST_LEVEL_DEBUG: -#ifdef QT_DEBUG - case GST_LEVEL_LOG: - case GST_LEVEL_TRACE: - case GST_LEVEL_MEMDUMP: -#endif - log.debug(GStreamerAPILog, "%s %s", object_info, gst_debug_message_get(message)); - break; - default: - break; - } +QString _cleanJoin(const QString &base, const QString &relative) +{ + return QDir::cleanPath(QDir(base).filePath(relative)); +} - g_clear_pointer(&object_info, g_free); +void _setGstEnv(const char *name, const QString &value) +{ + qputenv(name, value.toUtf8()); + qCDebug(GStreamerLog) << " " << name << "=" << value; } -void _setGstEnvVars() +void _unsetEnv(const char *name) { - const QString appDir = QCoreApplication::applicationDirPath(); - qCDebug(GStreamerLog) << "App Directory:" << appDir; - -#if defined(Q_OS_MACOS) && defined(QGC_GST_MACOS_FRAMEWORK) - const QString frameworkDir = QDir(appDir).filePath("../Frameworks/GStreamer.framework"); - const QString rootDir = QDir(frameworkDir).filePath("Versions/1.0"); - const QString libDir = QDir(rootDir).filePath("../lib"); - const QString pluginDir = QDir(libDir).filePath("gstreamer-1.0"); - const QString gioMod = QDir(libDir).filePath("gio/modules"); - const QString libexecDir = QDir(appDir).filePath("../libexec"); - const QString scanner = QDir(libexecDir).filePath("gstreamer-1.0/gst-plugin-scanner"); - const QString ptp = QDir(libexecDir).filePath("gstreamer-1.0/gst-ptp-helper"); - - if (QFileInfo::exists(frameworkDir)) { - qputenv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", "no"); - qputenv("GIO_EXTRA_MODULES", gioMod.toUtf8().constData()); - qputenv("GST_PTP_HELPER_1_0", ptp.toUtf8().constData()); - qputenv("GST_PTP_HELPER", ptp.toUtf8().constData()); - qputenv("GST_PLUGIN_SCANNER_1_0", scanner.toUtf8().constData()); - qputenv("GST_PLUGIN_SCANNER", scanner.toUtf8().constData()); - qputenv("GST_PLUGIN_SYSTEM_PATH_1_0", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_SYSTEM_PATH", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_PATH_1_0", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_PATH", pluginDir.toUtf8().constData()); - qputenv("GTK_PATH", rootDir.toUtf8().constData()); - } -#elif defined(Q_OS_WIN) - const QString binDir = appDir; - const QString libDir = QDir(binDir).filePath("../lib"); - const QString pluginDir = QDir(libDir).filePath("gstreamer-1.0"); - const QString gioMod = QDir(libDir).filePath("gio/modules"); - const QString libexecDir = QDir(binDir).filePath("../libexec"); - const QString scanner = QDir(libexecDir).filePath("gstreamer-1.0/gst-plugin-scanner"); - const QString ptp = QDir(libexecDir).filePath("gstreamer-1.0/gst-ptp-helper"); - - if (QFileInfo::exists(pluginDir)) { - qputenv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", "no"); - qputenv("GIO_EXTRA_MODULES", gioMod.toUtf8().constData()); - qputenv("GST_PTP_HELPER_1_0", ptp.toUtf8().constData()); - qputenv("GST_PTP_HELPER", ptp.toUtf8().constData()); - qputenv("GST_PLUGIN_SCANNER_1_0", scanner.toUtf8().constData()); - qputenv("GST_PLUGIN_SCANNER", scanner.toUtf8().constData()); - qputenv("GST_PLUGIN_SYSTEM_PATH_1_0", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_SYSTEM_PATH", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_PATH_1_0", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_PATH", pluginDir.toUtf8().constData()); + if (qEnvironmentVariableIsSet(name)) { + qunsetenv(name); + qCDebug(GStreamerLog) << " unset" << name; } -#endif } -void _logPlugin(gpointer data, gpointer /*user_data*/) +void _setGstEnvIfExists(const char *name, const QString &path) { - GstPlugin *plugin = static_cast(data); - if (!plugin) { - return; + if (QFileInfo::exists(path)) { + _setGstEnv(name, path); } - - const gchar *name = gst_plugin_get_name(plugin); - const gchar *version = gst_plugin_get_version(plugin); - qCDebug(GStreamerLog) << " " << name << "-" << version; } -bool _verifyPlugins() +bool _isExecutableFile(const QString &path) { - bool result = true; - - GstRegistry *registry = gst_registry_get(); - - qCDebug(GStreamerLog) << "Installed GStreamer Plugins:"; - GList *plugins = gst_registry_get_plugin_list(registry); - g_list_foreach(plugins, _logPlugin, NULL); - g_list_free(plugins); + const QFileInfo fileInfo(path); + return fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable(); +} - static constexpr const char *pluginNames[2] = {"qml6", "qgc"}; - for (const char *name : pluginNames) { - GstPlugin *plugin = gst_registry_find_plugin(registry, name); - if (!plugin) { - qCCritical(GStreamerLog) << name << "plugin NOT found."; - result = false; - continue; +QString _firstExistingPath(const QStringList &paths) +{ + for (const QString &path : paths) { + if (QFileInfo::exists(path)) { + return path; } - gst_clear_object(&plugin); } - if (!result) { - QString pluginPath; - if (qEnvironmentVariableIsSet("GST_PLUGIN_PATH_1_0")) { - pluginPath = qgetenv("GST_PLUGIN_PATH_1_0"); - } else if (qEnvironmentVariableIsSet("GST_PLUGIN_PATH")) { - pluginPath = qgetenv("GST_PLUGIN_PATH"); - } + return {}; +} -#ifdef QGC_GST_STATIC_BUILD - qCCritical(GStreamerLog) << "Please update the list of static plugins in GStreamer.cc"; -#else - if (!pluginPath.isEmpty()) { - qCCritical(GStreamerLog) << "Please check in GST_PLUGIN_PATH=" << pluginPath; - } else { - qCCritical(GStreamerLog) << "Please set GST_PLUGIN_PATH to the path of your plugin"; +QString _joinExistingPaths(const QStringList &paths) +{ + QStringList existing; + existing.reserve(paths.size()); + + for (const QString &path : paths) { + if (QFileInfo::exists(path) && !existing.contains(path)) { + existing.append(path); } -#endif } - return result; + return existing.join(QDir::listSeparator()); } -void _logDecoderRanks() +void _clearManagedGstEnvVars() { - GList *decoderFactories = gst_element_factory_list_get_elements( - static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), - GST_RANK_NONE); + static constexpr const char *varsToUnset[] = { + "GIO_EXTRA_MODULES", + "GIO_MODULE_DIR", + "GIO_USE_VFS", + "GST_PTP_HELPER_1_0", + "GST_PTP_HELPER", + "GST_PLUGIN_SCANNER_1_0", + "GST_PLUGIN_SCANNER", + "GST_PLUGIN_SYSTEM_PATH_1_0", + "GST_PLUGIN_SYSTEM_PATH", + "GST_PLUGIN_PATH_1_0", + "GST_PLUGIN_PATH", + "PYTHONNOUSERSITE", + }; + + for (const char *name : varsToUnset) { + _unsetEnv(name); + } +} - if (!decoderFactories) { - qCDebug(GStreamerDecoderRanksLog) << "No video decoder factories found."; - return; +void _setGstEnvIfExecutable(const char *name, const QString &path) +{ + if (_isExecutableFile(path)) { + _setGstEnv(name, path); + } else { + _unsetEnv(name); } +} - decoderFactories = g_list_sort(decoderFactories, [](gconstpointer lhs, gconstpointer rhs) -> gint { - GstElementFactory *lhsFactory = GST_ELEMENT_FACTORY(lhs); - GstElementFactory *rhsFactory = GST_ELEMENT_FACTORY(rhs); +static constexpr const char *kPythonEnvVars[] = { + "PYTHONHOME", + "PYTHONPATH", + "VIRTUAL_ENV", + "CONDA_PREFIX", + "CONDA_DEFAULT_ENV", + "PYTHONUSERBASE", + "PYTHONNOUSERSITE", +}; - if (!lhsFactory && !rhsFactory) { - return 0; - } - if (!lhsFactory) { - return 1; - } - if (!rhsFactory) { - return -1; - } +QHash s_savedPythonEnv; +bool s_pythonEnvSanitized = false; + +void _sanitizePythonEnvForScanner() +{ + s_savedPythonEnv.clear(); + s_pythonEnvSanitized = true; - const guint lhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(lhsFactory)); - const guint rhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(rhsFactory)); - if (lhsRank == rhsRank) { - const gchar *lhsName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(lhsFactory)); - const gchar *rhsName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(rhsFactory)); - return g_strcmp0(lhsName, rhsName); + for (const char *name : kPythonEnvVars) { + if (qEnvironmentVariableIsSet(name)) { + s_savedPythonEnv.insert(QByteArray(name), qgetenv(name)); } + _unsetEnv(name); + } - return lhsRank > rhsRank ? -1 : 1; - }); + _setGstEnv("PYTHONNOUSERSITE", QStringLiteral("1")); +} - qCDebug(GStreamerDecoderRanksLog) << "Video decoder plugin ranks:"; - for (GList *node = decoderFactories; node != nullptr; node = node->next) { - GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); - if (!factory) { - continue; - } +void _restorePythonEnv() +{ + if (!s_pythonEnvSanitized) { + return; + } + s_pythonEnvSanitized = false; - const gchar *decoderKlass = gst_element_factory_get_klass(factory); - GstPluginFeature *feature = GST_PLUGIN_FEATURE(factory); - const gchar *featureName = gst_plugin_feature_get_name(feature); - const guint rank = gst_plugin_feature_get_rank(feature); + qCDebug(GStreamerLog) << "Restoring Python environment variables"; - GstPlugin *plugin = gst_plugin_feature_get_plugin(feature); - if (plugin) { - qCDebug(GStreamerDecoderRanksLog) << " " << gst_plugin_get_name(plugin) << "/" << featureName << "-" << decoderKlass << ":" << rank; - gst_object_unref(plugin); + for (const char *name : kPythonEnvVars) { + const QByteArray key(name); + if (s_savedPythonEnv.contains(key)) { + qputenv(name, s_savedPythonEnv.value(key)); + qCDebug(GStreamerLog) << " " << name << "=" << s_savedPythonEnv.value(key); } else { - qCDebug(GStreamerDecoderRanksLog) << " " << featureName << "-" << decoderKlass << ":" << rank; + _unsetEnv(name); } } - gst_plugin_feature_list_free(decoderFactories); + s_savedPythonEnv.clear(); } -void _lowerSoftwareDecoderRanks(GstRegistry *registry) +void _applyGstEnvVars(const QString &pluginDir, const QString &gioModDir, + const QString &scannerPath, const QString &ptpPath) { - static constexpr uint16_t NewRank = GST_RANK_NONE; - if (!registry) { - qCCritical(GStreamerLog) << "Invalid registry!"; - return; - } - - const char* softDecoders[] = {"avdec_h264", "avdec_h265", "avdec_mjpeg", "avdec_mpeg2video", "avdec_mpeg4", - "avdec_vp8", "avdec_vp9", "dav1ddec", "vp8dec", "vp9dec"}; + qCDebug(GStreamerLog) << "Applying GStreamer environment:"; + + _clearManagedGstEnvVars(); + _sanitizePythonEnvForScanner(); + // Force fresh registry scan since we're redirecting plugin paths to bundled locations + _setGstEnv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", QStringLiteral("no")); + _setGstEnvIfExists("GIO_EXTRA_MODULES", gioModDir); + _setGstEnvIfExecutable("GST_PTP_HELPER_1_0", ptpPath); + _setGstEnvIfExecutable("GST_PTP_HELPER", ptpPath); + _setGstEnvIfExecutable("GST_PLUGIN_SCANNER_1_0", scannerPath); + _setGstEnvIfExecutable("GST_PLUGIN_SCANNER", scannerPath); + _setGstEnv("GST_PLUGIN_SYSTEM_PATH_1_0", pluginDir); + _setGstEnv("GST_PLUGIN_SYSTEM_PATH", pluginDir); + _setGstEnv("GST_PLUGIN_PATH_1_0", pluginDir); + _setGstEnv("GST_PLUGIN_PATH", pluginDir); +} - for (const char *name : softDecoders) { - GstPluginFeature *feature = gst_registry_lookup_feature(registry, name); - if (feature) { - qCDebug(GStreamerLog) << "Setting software decoder rank low:" << name << " rank:" << NewRank; - gst_plugin_feature_set_rank(feature, NewRank); - gst_object_unref(feature); - } else { - qCDebug(GStreamerLog) << "Software decoder not found:" << name; +#if defined(Q_OS_LINUX) +bool _systemGioIsNew() +{ + // Probe the system GIO library on disk (not the in-process symbols) for + // g_task_set_static_name, which was added in GIO 2.76. This mirrors + // AppRun's `nm -D "$SYSTEM_GIO" | grep g_task_set_static_name` check. + static constexpr const char *kGioSoPaths[] = { + "/usr/lib/x86_64-linux-gnu/libgio-2.0.so.0", + "/usr/lib/aarch64-linux-gnu/libgio-2.0.so.0", + "/usr/lib64/libgio-2.0.so.0", + "/usr/lib/libgio-2.0.so.0", + }; + + for (const char *path : kGioSoPaths) { + void *handle = dlopen(path, RTLD_LAZY | RTLD_NOLOAD); + if (!handle) { + handle = dlopen(path, RTLD_LAZY); + } + if (!handle) { + continue; } + const bool found = (dlsym(handle, "g_task_set_static_name") != nullptr); + dlclose(handle); + return found; } + + return false; } -void _changeFeatureRank(GstRegistry *registry, const char *featureName, uint16_t rank) +void _applyGioCompatOverride(const QString &gioModDir) { - if (!registry || !featureName) { + if (gioModDir.isEmpty()) { return; } - GstPluginFeature *feature = gst_registry_lookup_feature(registry, featureName); - if (!feature) { - qCDebug(GStreamerLog) << "Failed to change ranking of feature. Feature does not exist:" << featureName; - return; + // GIO 2.76+ requires bundled modules to be loaded via GIO_MODULE_DIR with + // VFS forced to local, mirroring the AppImage launcher logic. + if (_systemGioIsNew()) { + _unsetEnv("GIO_EXTRA_MODULES"); + _setGstEnv("GIO_MODULE_DIR", gioModDir); + _setGstEnv("GIO_USE_VFS", QStringLiteral("local")); } +} +#endif - qCDebug(GStreamerLog) << " Changing feature (" << featureName << ") to use rank:" << rank; - gst_plugin_feature_set_rank(feature, rank); - gst_clear_object(&feature); +void _warnIfScannerMissing(const QString &platformLabel, const QString &scannerPath) +{ + if (scannerPath.isEmpty()) { + qCWarning(GStreamerLog) << "GStreamer:" << platformLabel + << "bundled gst-plugin-scanner not found; GStreamer will use in-process scanning"; + } else if (!_isExecutableFile(scannerPath)) { + qCWarning(GStreamerLog) << "GStreamer:" << platformLabel + << "gst-plugin-scanner is not executable:" << scannerPath; + } } -void _prioritizeByHardwareClass(GstRegistry *registry, uint16_t prioritizedRank, bool requireHardware) +bool _validateMacBundlePaths(const QString &bundleFrameworkRoot, + const QString &pluginDirs, + const QString &scannerPath) { - if (!registry) { - qCCritical(GStreamerLog) << "Failed to get gstreamer registry."; - return; + if (pluginDirs.isEmpty()) { + _setEnvValidationError(QStringLiteral( + "GStreamer: bundled macOS framework found but plugin directory is missing under %1") + .arg(bundleFrameworkRoot)); + return false; } - GList *decoderFactories = gst_element_factory_list_get_elements( - static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), - GST_RANK_NONE); + _warnIfScannerMissing(QStringLiteral("macOS framework"), scannerPath); + return true; +} - if (!decoderFactories) { - qCDebug(GStreamerLog) << "No decoder factories available while prioritizing" - << (requireHardware ? "hardware" : "software") << "decoders"; - return; +bool _validateBundledDesktopPaths(const QString &platformLabel, + const QString &pluginDirs, + const QString &scannerPath) +{ + if (pluginDirs.isEmpty()) { + _setEnvValidationError(QStringLiteral( + "GStreamer: %1 bundled plugin directory is missing.") + .arg(platformLabel)); + return false; } - qCDebug(GStreamerLog) << "Prioritizing" << (requireHardware ? "hardware" : "software") - << "video decoders with rank:" << prioritizedRank; - int matchedFactories = 0; - for (GList *node = decoderFactories; node != nullptr; node = node->next) { - GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); - if (!factory) { - continue; - } + _warnIfScannerMissing(platformLabel, scannerPath); + return true; +} - if (GStreamer::is_hardware_decoder_factory(factory) != requireHardware) { - continue; - } +void _setGstEnvVars() +{ + _resetEnvValidation(); - const gchar *featureName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory)); - if (!featureName) { - continue; - } + const QString appDir = QCoreApplication::applicationDirPath(); + qCDebug(GStreamerLog) << "App directory:" << appDir; + +#if defined(Q_OS_MACOS) + const QString frameworkDir = _cleanJoin(appDir, "../Frameworks/GStreamer.framework"); + const QString rootDir = _firstExistingPath({ + _cleanJoin(frameworkDir, "Versions/1.0"), + _cleanJoin(frameworkDir, "Versions/Current"), + frameworkDir, + }); + +#if defined(QGC_GST_MACOS_FRAMEWORK) + // Framework builds prefer framework paths over app-relative paths + const QString pluginDirs = _joinExistingPaths({ + _cleanJoin(rootDir, "lib/gstreamer-1.0"), + _cleanJoin(appDir, "../lib/gstreamer-1.0"), + }); + const QString gioMod = _firstExistingPath({ + _cleanJoin(rootDir, "lib/gio/modules"), + _cleanJoin(appDir, "../lib/gio/modules"), + }); +#else + // Non-framework (Homebrew) builds prefer app-relative paths + const QString pluginDirs = _joinExistingPaths({ + _cleanJoin(appDir, "../lib/gstreamer-1.0"), + _cleanJoin(rootDir, "lib/gstreamer-1.0"), + }); + const QString gioMod = _firstExistingPath({ + _cleanJoin(appDir, "../lib/gio/modules"), + _cleanJoin(rootDir, "lib/gio/modules"), + }); +#endif + + const QString scanner = _firstExistingPath({ + _cleanJoin(appDir, "../libexec/gstreamer-1.0/gst-plugin-scanner"), + _cleanJoin(rootDir, "libexec/gstreamer-1.0/gst-plugin-scanner"), + }); + const QString ptp = _firstExistingPath({ + _cleanJoin(appDir, "../libexec/gstreamer-1.0/gst-ptp-helper"), + _cleanJoin(rootDir, "libexec/gstreamer-1.0/gst-ptp-helper"), + }); + const bool hasBundledFramework = QFileInfo::exists(frameworkDir); + + const bool validBundlePaths = hasBundledFramework + ? _validateMacBundlePaths(rootDir, pluginDirs, scanner) + : !pluginDirs.isEmpty(); + + if (!pluginDirs.isEmpty() && validBundlePaths) { + _applyGstEnvVars(pluginDirs, gioMod, scanner, ptp); + } - _changeFeatureRank(registry, featureName, prioritizedRank); - ++matchedFactories; +#if defined(QGC_GST_MACOS_FRAMEWORK) + if (hasBundledFramework) { + _setGstEnv("GTK_PATH", rootDir); } +#endif - if (matchedFactories == 0) { - qCWarning(GStreamerLog) << "No" << (requireHardware ? "hardware" : "software") - << "video decoder factories found to reprioritize."; +#elif defined(Q_OS_WIN) + const QString libDir = _cleanJoin(appDir, "../lib"); + const QString pluginDir = _cleanJoin(libDir, "gstreamer-1.0"); + const QString gioMod = _cleanJoin(libDir, "gio/modules"); + const QString libexecDir = _cleanJoin(appDir, "../libexec"); + const QString scanner = _cleanJoin(libexecDir, "gstreamer-1.0/gst-plugin-scanner.exe"); + const QString ptp = _cleanJoin(libexecDir, "gstreamer-1.0/gst-ptp-helper.exe"); + + if (QFileInfo::exists(pluginDir) + && _validateBundledDesktopPaths(QStringLiteral("Windows"), pluginDir, scanner)) { + _applyGstEnvVars(pluginDir, gioMod, scanner, ptp); } - // Lower software decoder rank when using hardware decoders - if(requireHardware) { - qCCritical(GstVideoReceiverLog) << "Set the software decoder rank low."; - _lowerSoftwareDecoderRanks(registry); +#elif defined(Q_OS_LINUX) + // AppRun sets GStreamer env vars before launch (including GIO compatibility + // logic). Only apply fallback paths when no external override is present. + if (!qEnvironmentVariableIsSet("GST_PLUGIN_PATH_1_0") + && !qEnvironmentVariableIsSet("GST_PLUGIN_PATH") + && !qEnvironmentVariableIsSet("GST_PLUGIN_SYSTEM_PATH_1_0") + && !qEnvironmentVariableIsSet("GST_PLUGIN_SYSTEM_PATH")) { + const QString libDir = _cleanJoin(appDir, "../lib"); + const QString libexecDir = _cleanJoin(appDir, "../libexec"); + const QString pluginDir = _cleanJoin(libDir, "gstreamer-1.0"); + const QString gioMod = _cleanJoin(libDir, "gio/modules"); + const QString scanner = _firstExistingPath({ + _cleanJoin(libDir, "gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"), + _cleanJoin(libexecDir, "gstreamer-1.0/gst-plugin-scanner"), + }); + const QString ptp = _firstExistingPath({ + _cleanJoin(libDir, "gstreamer1.0/gstreamer-1.0/gst-ptp-helper"), + _cleanJoin(libexecDir, "gstreamer-1.0/gst-ptp-helper"), + }); + + if (QFileInfo::exists(pluginDir) + && _validateBundledDesktopPaths(QStringLiteral("Linux"), pluginDir, scanner)) { + _applyGstEnvVars(pluginDir, gioMod, scanner, ptp); + _applyGioCompatOverride(gioMod); + } } +#endif - gst_plugin_feature_list_free(decoderFactories); } -void _setCodecPriorities(GStreamer::VideoDecoderOptions option) +bool _verifyPlugins() { GstRegistry *registry = gst_registry_get(); - if (!registry) { - qCCritical(GStreamerLog) << "Failed to get gstreamer registry."; - return; + qCCritical(GStreamerLog) << "Failed to get GStreamer registry"; + return false; } - static constexpr uint16_t PrioritizedRank = GST_RANK_PRIMARY + 1; - - // TODO: ForceVideoDecoderCustom in VideoSettings with textbox in QML - - switch (option) { - case GStreamer::ForceVideoDecoderDefault: - break; - case GStreamer::ForceVideoDecoderSoftware: - _prioritizeByHardwareClass(registry, PrioritizedRank, false); - break; - case GStreamer::ForceVideoDecoderHardware: - _prioritizeByHardwareClass(registry, PrioritizedRank, true); - break; - case GStreamer::ForceVideoDecoderVAAPI: - for (const char *name : {"vaav1dec", "vah264dec", "vah265dec", "vajpegdec", "vampeg2dec", "vavp8dec", "vavp9dec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); - } - break; - case GStreamer::ForceVideoDecoderNVIDIA: - for (const char *name : {"nvav1dec", "nvh264dec", "nvh265dec", "nvjpegdec", "nvmpeg2videodec", "nvmpeg4videodec", "nvmpegvideodec", "nvvp8dec", "nvvp9dec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); + GList *plugins = gst_registry_get_plugin_list(registry); + if (plugins) { + qCDebug(GStreamerLog) << "Installed GStreamer plugins:"; + for (GList *node = plugins; node != nullptr; node = node->next) { + GstPlugin *plugin = static_cast(node->data); + if (plugin) { + qCDebug(GStreamerLog) << " " << gst_plugin_get_name(plugin) + << gst_plugin_get_version(plugin); + } } - break; - case GStreamer::ForceVideoDecoderDirectX3D: - for (const char *name : {"d3d11av1dec", "d3d11h264dec", "d3d11h265dec", "d3d11mpeg2dec", "d3d11vp8dec", "d3d11vp9dec", - "d3d12av1dec", "d3d12h264dec", "d3d12h265dec", "d3d12mpeg2dec", "d3d12vp8dec", "d3d12vp9dec", - "dxvaav1decoder", "dxvah264decoder", "dxvah265decoder", "dxvampeg2decoder", "dxvavp8decoder", "dxvavp9decoder" }) { - _changeFeatureRank(registry, name, PrioritizedRank); + gst_plugin_list_free(plugins); + } + + bool result = true; + static constexpr const char *requiredPlugins[] = {"qml6", "qgc", "coreelements"}; + for (const char *name : requiredPlugins) { + GstPlugin *plugin = gst_registry_find_plugin(registry, name); + if (!plugin) { + qCCritical(GStreamerLog) << "Required QGC plugin not found:" << name; + result = false; + continue; } - break; - case GStreamer::ForceVideoDecoderVideoToolbox: - for (const char *name : {"vtdec_hw", "vtdec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); + gst_clear_object(&plugin); + } + + if (!result) { + const QByteArray pluginPath = qEnvironmentVariableIsSet("GST_PLUGIN_PATH_1_0") + ? qgetenv("GST_PLUGIN_PATH_1_0") + : qgetenv("GST_PLUGIN_PATH"); + + if (!pluginPath.isEmpty()) { + qCCritical(GStreamerLog) << "Check GST_PLUGIN_PATH=" << pluginPath; + } else { + qCCritical(GStreamerLog) << "GST_PLUGIN_PATH is not set"; } - break; - case GStreamer::ForceVideoDecoderIntel: - for (const char *name : {"qsvh264dec", "qsvh265dec", "qsvjpegdec", "qsvvp9dec", "msdkav1dec", "msdkh264dec", "msdkh265dec", "msdkmjpegdec", "msdkmpeg2dec", "msdkvc1dec", "msdkvp8dec", "msdkvp9dec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); + + GList *allPlugins = gst_registry_get_plugin_list(registry); + for (GList *node = allPlugins; node != nullptr; node = node->next) { + GstPlugin *p = static_cast(node->data); + if (!p) continue; + const gchar *desc = gst_plugin_get_description(p); + const gchar *filename = gst_plugin_get_filename(p); + if (desc && g_str_has_prefix(desc, "BLACKLIST")) { + qCWarning(GStreamerLog) << "Blacklisted plugin:" << gst_plugin_get_name(p) + << "file:" << (filename ? filename : "(null)"); + } } - break; - case GStreamer::ForceVideoDecoderVulkan: - for (const char *name : {"vulkanh264dec", "vulkanh265dec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); + gst_plugin_list_free(allPlugins); + + static constexpr const char *envDiagnostics[] = { + "GST_PLUGIN_PATH", "GST_PLUGIN_PATH_1_0", + "GST_PLUGIN_SYSTEM_PATH", "GST_PLUGIN_SYSTEM_PATH_1_0", + "GST_PLUGIN_SCANNER", "GST_PLUGIN_SCANNER_1_0", + "GST_REGISTRY_REUSE_PLUGIN_SCANNER", + }; + qCCritical(GStreamerLog) << "GStreamer environment diagnostics:"; + for (const char *var : envDiagnostics) { + const QByteArray val = qgetenv(var); + qCCritical(GStreamerLog) << " " << var << "=" << (val.isEmpty() ? "(unset)" : val.constData()); } - break; - default: - qCWarning(GStreamerLog) << "Can't handle decode option:" << option; - break; } + + return result; } -bool initialize() + +void prepareEnvironment() { _setGstEnvVars(); +} - if (qEnvironmentVariableIsEmpty("GST_DEBUG")) { - int gstDebugLevel = 0; - QSettings settings; - if (settings.contains(AppSettings::gstDebugLevelName)) { - gstDebugLevel = settings.value(AppSettings::gstDebugLevelName).toInt(); - } - gst_debug_set_default_threshold(static_cast(gstDebugLevel)); +bool _initGstRuntime() +{ + if (!s_envPathsValid.load(std::memory_order_acquire)) { + const QMutexLocker locker(&s_envPathsMutex); + qCCritical(GStreamerLog) << "Invalid GStreamer environment configuration:" << s_envPathsError; + return false; } - gst_debug_remove_log_function(gst_debug_log_default); - gst_debug_add_log_function(_qtGstLog, nullptr, nullptr); - - const QStringList args = QCoreApplication::arguments(); - int gstArgc = args.size(); - - QByteArrayList argData; - argData.reserve(gstArgc); - - QVarLengthArray rawArgv; - rawArgv.reserve(gstArgc); + QByteArrayList argStorage; + for (const QString &arg : QCoreApplication::arguments()) { + argStorage.append(arg.toUtf8()); + } - for (const QString &arg : args) { - argData.append(arg.toUtf8()); - rawArgv.append(argData.last().data()); + QVarLengthArray argv; + for (QByteArray &arg : argStorage) { + argv.append(arg.data()); } - char **argvPtr = rawArgv.data(); + int argc = argv.size(); + char **argvPtr = argv.data(); GError *error = nullptr; - const gboolean ok = gst_init_check(&gstArgc, &argvPtr, &error); - if (!ok) { - qCritical(GStreamerLog) << "Failed to initialize GStreamer:" << error->message; + + if (!gst_init_check(&argc, &argvPtr, &error)) { + qCCritical(GStreamerLog) << "Failed to initialize GStreamer:" + << (error ? error->message : "unknown error"); g_clear_error(&error); + _restorePythonEnv(); + return false; + } + + _restorePythonEnv(); + return true; +} + +bool completeInit() +{ + if (!gst_is_initialized()) { + qCCritical(GStreamerLog) << "completeInit called but gst_init() has not been called"; return false; } - const gchar *version = gst_version_string(); - qCDebug(GStreamerLog) << QString("GStreamer Initialized (Version: %1)").arg(version); + GStreamer::configureDebugLogging(); + + gchar *version = gst_version_string(); + qCDebug(GStreamerLog) << "GStreamer initialized:" << version; + g_free(version); _registerPlugins(); if (!_verifyPlugins()) { - qCCritical(GStreamerLog) << "Failed to verify plugins - Check your GStreamer setup"; + qCCritical(GStreamerLog) << "Plugin verification failed"; return false; } - _logDecoderRanks(); - _setCodecPriorities(static_cast(SettingsManager::instance()->videoSettings()->forceVideoDecoder()->rawValue().toInt())); + GStreamer::logDecoderRanks(); + GStreamer::setCodecPriorities(static_cast( + SettingsManager::instance()->videoSettings()->forceVideoDecoder()->rawValue().toInt())); GstElement *sink = gst_element_factory_make("qml6glsink", nullptr); if (!sink) { - qCCritical(GStreamerLog) << "failed to init qml6glsink"; + qCCritical(GStreamerLog) << "Failed to create qml6glsink element"; return false; } - gst_clear_object(&sink); + + if (GStreamer::didExternalPluginLoaderFail()) { + qCCritical(GStreamerLog) + << "GStreamer external plugin loader failed. Check GST_PLUGIN_SCANNER and bundled runtime paths."; + return false; + } + return true; } +bool initialize() +{ + prepareEnvironment(); + + GStreamer::resetExternalPluginLoaderFailure(); + GStreamer::redirectGLibLogging(); + GStreamer::configureDebugLogging(); + + if (!_initGstRuntime()) { + return false; + } + + return completeInit(); +} + void *createVideoSink(QQuickItem *widget, QObject * /*parent*/) { GstElement *videoSinkBin = gst_element_factory_make("qgcvideosinkbin", NULL); @@ -565,6 +696,7 @@ void *createVideoSink(QQuickItem *widget, QObject * /*parent*/) void releaseVideoSink(void *sink) { + if (!sink) return; GstElement *videoSink = GST_ELEMENT(sink); gst_clear_object(&videoSink); } diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h index 763be20daf79..d20aaccaa9e8 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h @@ -1,10 +1,5 @@ #pragma once -#include - -Q_DECLARE_LOGGING_CATEGORY(GStreamerLog) -Q_DECLARE_LOGGING_CATEGORY(GStreamerAPILog) - class QQuickItem; class VideoReceiver; @@ -23,9 +18,17 @@ enum VideoDecoderOptions { ForceVideoDecoderHardware }; +// Call prepareEnvironment() on the main thread before dispatching initialize() +// to a worker thread. It sets process-wide env vars that gst_init reads. +void prepareEnvironment(); + +// Desktop: call initialize() which runs gst_init_check() + post-init steps. +// Android: Java calls gst_init(); call completeInit() after it succeeds. +// Both run on a worker thread — avoid accessing QObject singletons inside. bool initialize(); +bool completeInit(); void *createVideoSink(QQuickItem *widget, QObject *parent = nullptr); void releaseVideoSink(void *sink); VideoReceiver *createVideoReceiver(QObject *parent = nullptr); -}; +} diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc index bc567e081f1d..d8717b6aa407 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc @@ -1,15 +1,20 @@ #include "GStreamerHelpers.h" +#include "GStreamerLogging.h" #include +#include #include -#include namespace GStreamer { gboolean -is_valid_rtsp_uri(const gchar *uri_str) +isValidRtspUri(const gchar *uri_str) { + if (!uri_str) { + return FALSE; + } + GstRTSPUrl *url = NULL; GstRTSPResult res; @@ -19,14 +24,18 @@ is_valid_rtsp_uri(const gchar *uri_str) res = gst_rtsp_url_parse(uri_str, &url); if ((res != GST_RTSP_OK) || (url == NULL)) { + if (url) { + gst_rtsp_url_free(url); + } return FALSE; } + const gboolean hasHost = (url->host && url->host[0] != '\0'); gst_rtsp_url_free(url); - return TRUE; + return hasHost; } -bool is_hardware_decoder_factory(GstElementFactory *factory) +bool isHardwareDecoderFactory(GstElementFactory *factory) { if (!factory) { return false; @@ -37,38 +46,45 @@ bool is_hardware_decoder_factory(GstElementFactory *factory) return false; } - // Exclude Android software decoders (OMXGoogle / C2Android) - QString name = QString::fromUtf8(factoryName).toLower(); - if (name.startsWith("amcviddec-omxgoogle") || name.startsWith("amcviddec-c2android")) { + const QString nameLower = QString::fromUtf8(factoryName).toLower(); + + // Android MediaCodec: exclude software wrappers, accept remaining as hardware + if (nameLower.startsWith("amcviddec-omxgoogle") || nameLower.startsWith("amcviddec-c2android")) { return false; } + if (nameLower.startsWith("amcviddec-")) { + return true; + } const auto containsHardware = [](const gchar *value) { - return value && (g_strrstr(value, "Hardware") != nullptr || g_strrstr(value, "hardware") != nullptr); + if (!value) return false; + gchar *lower = g_ascii_strdown(value, -1); + bool found = (g_strrstr(lower, "hardware") != nullptr); + g_free(lower); + return found; }; if (containsHardware(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_KLASS))) { return true; } - if (containsHardware(gst_element_factory_get_klass(factory))) { + if (containsHardware(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_DESCRIPTION))) { return true; } - const QString nameLower = QString::fromUtf8(factoryName).toLower(); - static const QStringList kHardwareTags = { - QStringLiteral("va"), // vaapi family - QStringLiteral("nv"), // nvidia nvcodec - QStringLiteral("qsv"), // intel quick sync - QStringLiteral("msdk"), // intel media sdk - QStringLiteral("vulkan"), // vulkan accelerated - QStringLiteral("d3d"), // direct3d - QStringLiteral("dxva"), // directx video accel - QStringLiteral("vtdec"), // apple video toolbox - QStringLiteral("metal") // metal-based decoders + static constexpr QLatin1String kHardwareTags[] = { + QLatin1String("va"), + QLatin1String("nv"), + QLatin1String("qsv"), + QLatin1String("msdk"), + QLatin1String("vulkan"), + QLatin1String("d3d"), + QLatin1String("dxva"), + QLatin1String("vtdec"), + QLatin1String("metal"), }; - for (const QString &tag : kHardwareTags) { + for (const auto &tag : kHardwareTags) { if (nameLower.contains(tag)) { return true; } @@ -77,4 +93,225 @@ bool is_hardware_decoder_factory(GstElementFactory *factory) return false; } +namespace { + +void changeFeatureRank(GstRegistry *registry, const char *featureName, uint16_t rank) +{ + if (!registry || !featureName) { + return; + } + + GstPluginFeature *feature = gst_registry_lookup_feature(registry, featureName); + if (!feature) { + qCDebug(GStreamerLog) << "Failed to change ranking of feature. Feature does not exist:" << featureName; + return; + } + + qCDebug(GStreamerLog) << " Changing feature (" << featureName << ") to use rank:" << rank; + gst_plugin_feature_set_rank(feature, rank); + gst_clear_object(&feature); +} + +void lowerSoftwareDecoderRanks(GstRegistry *registry) +{ + static constexpr uint16_t NewRank = GST_RANK_NONE; + if (!registry) { + qCCritical(GStreamerLog) << "Invalid registry!"; + return; + } + + GList *decoderFactories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + for (GList *node = decoderFactories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + if (!factory) { + continue; + } + + if (GStreamer::isHardwareDecoderFactory(factory)) { + continue; + } + + const gchar *name = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory)); + if (!name) { + continue; + } + + qCDebug(GStreamerLog) << "Setting software decoder rank low:" << name << " rank:" << NewRank; + gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(factory), NewRank); + } + + gst_plugin_feature_list_free(decoderFactories); +} + +void prioritizeByHardwareClass(GstRegistry *registry, uint16_t prioritizedRank, bool requireHardware) +{ + if (!registry) { + qCCritical(GStreamerLog) << "Failed to get gstreamer registry."; + return; + } + + GList *decoderFactories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!decoderFactories) { + qCDebug(GStreamerLog) << "No decoder factories available while prioritizing" + << (requireHardware ? "hardware" : "software") << "decoders"; + return; + } + + qCDebug(GStreamerLog) << "Prioritizing" << (requireHardware ? "hardware" : "software") + << "video decoders with rank:" << prioritizedRank; + int matchedFactories = 0; + for (GList *node = decoderFactories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + if (!factory) { + continue; + } + + if (GStreamer::isHardwareDecoderFactory(factory) != requireHardware) { + continue; + } + + const gchar *featureName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory)); + if (!featureName) { + continue; + } + + changeFeatureRank(registry, featureName, prioritizedRank); + ++matchedFactories; + } + + if (matchedFactories == 0) { + qCWarning(GStreamerLog) << "No" << (requireHardware ? "hardware" : "software") + << "video decoder factories found to reprioritize."; + } + + if (requireHardware) { + qCDebug(GStreamerLog) << "Lowering software decoder ranks for hardware priority."; + lowerSoftwareDecoderRanks(registry); + } + + gst_plugin_feature_list_free(decoderFactories); +} + +} // anonymous namespace + +void setCodecPriorities(VideoDecoderOptions option) +{ + GstRegistry *registry = gst_registry_get(); + + if (!registry) { + qCCritical(GStreamerLog) << "Failed to get gstreamer registry."; + return; + } + + static constexpr uint16_t PrioritizedRank = GST_RANK_PRIMARY + 1; + + switch (option) { + case ForceVideoDecoderDefault: + // Android/iOS hardware decoders (amcviddec, vtdec) already have higher + // default ranks than software decoders, so decodebin tries them first. + // However, AMC decoders require GL output (GLMemory caps) which the + // current pipeline doesn't support — so they fail and decodebin falls + // back to software. Setting software to RANK_NONE here would eliminate + // that fallback entirely. Leave ranks untouched until the pipeline + // supports GL context sharing with Qt. + break; + case ForceVideoDecoderSoftware: + prioritizeByHardwareClass(registry, PrioritizedRank, false); + break; + case ForceVideoDecoderHardware: + prioritizeByHardwareClass(registry, PrioritizedRank, true); + break; + case ForceVideoDecoderVAAPI: + for (const char *name : {"vaav1dec", "vah264dec", "vah265dec", "vajpegdec", "vampeg2dec", "vavp8dec", "vavp9dec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderNVIDIA: + for (const char *name : {"nvav1dec", "nvh264dec", "nvh265dec", "nvjpegdec", "nvmpeg2videodec", "nvmpeg4videodec", "nvmpegvideodec", "nvvp8dec", "nvvp9dec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderDirectX3D: + for (const char *name : {"d3d11av1dec", "d3d11h264dec", "d3d11h265dec", "d3d11mpeg2dec", "d3d11vp8dec", "d3d11vp9dec", + "d3d12av1dec", "d3d12h264dec", "d3d12h265dec", "d3d12mpeg2dec", "d3d12vp8dec", "d3d12vp9dec", + "dxvaav1decoder", "dxvah264decoder", "dxvah265decoder", "dxvampeg2decoder", "dxvavp8decoder", "dxvavp9decoder"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderVideoToolbox: + for (const char *name : {"vtdec_hw", "vtdec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderIntel: + for (const char *name : {"qsvh264dec", "qsvh265dec", "qsvjpegdec", "qsvvp9dec", "msdkav1dec", "msdkh264dec", "msdkh265dec", "msdkmjpegdec", "msdkmpeg2dec", "msdkvc1dec", "msdkvp8dec", "msdkvp9dec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderVulkan: + for (const char *name : {"vulkanh264dec", "vulkanh265dec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + default: + qCWarning(GStreamerLog) << "Can't handle decode option:" << option; + break; + } +} + +void logDecoderRanks() +{ + GList *factories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!factories) { + qCDebug(GStreamerDecoderRanksLog) << "No video decoder factories found"; + return; + } + + factories = g_list_sort(factories, [](gconstpointer lhs, gconstpointer rhs) -> gint { + const guint lhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(lhs)); + const guint rhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(rhs)); + if (lhsRank != rhsRank) { + return (lhsRank > rhsRank) ? -1 : 1; + } + return g_strcmp0(gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(lhs)), + gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(rhs))); + }); + + qCDebug(GStreamerDecoderRanksLog) << "Video decoder ranks:"; + for (GList *node = factories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + GstPluginFeature *feature = GST_PLUGIN_FEATURE(factory); + const gchar *featureName = gst_plugin_feature_get_name(feature); + const guint rank = gst_plugin_feature_get_rank(feature); + const gchar *klass = gst_element_factory_get_klass(factory); + const bool isHw = GStreamer::isHardwareDecoderFactory(factory); + + GstPlugin *plugin = gst_plugin_feature_get_plugin(feature); + const gchar *pluginName = plugin ? gst_plugin_get_name(plugin) : "?"; + + qCDebug(GStreamerDecoderRanksLog).noquote() + << QStringLiteral(" [%1] %2/%3 rank=%4 (%5)") + .arg(isHw ? QStringLiteral("HW") : QStringLiteral("SW"), + QString::fromUtf8(pluginName), + QString::fromUtf8(featureName)) + .arg(rank) + .arg(QString::fromUtf8(klass)); + + if (plugin) { + gst_object_unref(plugin); + } + } + + gst_plugin_feature_list_free(factories); +} + } // namespace GStreamer diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h index 148128e417b4..7d807d73fe90 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h @@ -1,14 +1,16 @@ #pragma once -#include -// Needed for GstElementFactory #include +#include "GStreamer.h" + namespace GStreamer { - gboolean is_valid_rtsp_uri(const gchar *uri_str); + gboolean isValidRtspUri(const gchar *uri_str); + + bool isHardwareDecoderFactory(GstElementFactory *factory); + + void logDecoderRanks(); - // Returns true if the given factory likely represents a hardware-accelerated decoder. - // Heuristics: checks metadata/klass for "Hardware" and common vendor tags in the factory name. - bool is_hardware_decoder_factory(GstElementFactory *factory); + void setCodecPriorities(VideoDecoderOptions option); } diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc new file mode 100644 index 000000000000..10db5ee87f64 --- /dev/null +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc @@ -0,0 +1,151 @@ +#include "GStreamerLogging.h" +#include "AppSettings.h" +#include "QGCLoggingCategory.h" + +#include +#include + +#include +#include + +QGC_LOGGING_CATEGORY(GStreamerLog, "Video.GStreamer") +QGC_LOGGING_CATEGORY(GStreamerDecoderRanksLog, "Video.GStreamerDecoderRanks") +QGC_LOGGING_CATEGORY_ON(GStreamerAPILog, "Video.GStreamerAPI") + +namespace { + +std::atomic_bool g_externalPluginLoaderFailed {false}; + +void glib_print_handler(const gchar *string) +{ + qCInfo(GStreamerLog) << string; +} + +void glib_printerr_handler(const gchar *string) +{ + qCWarning(GStreamerLog) << string; +} + +void glib_log_handler(const gchar *log_domain, GLogLevelFlags log_level, + const gchar *message, gpointer user_data) +{ + Q_UNUSED(user_data); + const QString domain = log_domain ? QString::fromUtf8(log_domain) : QStringLiteral("GLib"); + const QString msg = QString::fromUtf8(message); + + if (msg.contains(QStringLiteral("External plugin loader failed"), Qt::CaseInsensitive)) { + g_externalPluginLoaderFailed.store(true); + } + + switch (log_level & G_LOG_LEVEL_MASK) { + case G_LOG_LEVEL_ERROR: + case G_LOG_LEVEL_CRITICAL: + qCCritical(GStreamerLog) << domain << msg; + break; + case G_LOG_LEVEL_WARNING: + qCWarning(GStreamerLog) << domain << msg; + break; + case G_LOG_LEVEL_MESSAGE: + case G_LOG_LEVEL_INFO: + qCInfo(GStreamerLog) << domain << msg; + break; + case G_LOG_LEVEL_DEBUG: + default: + qCDebug(GStreamerLog) << domain << msg; + break; + } +} + +} // anonymous namespace + +namespace GStreamer +{ + +void resetExternalPluginLoaderFailure() +{ + g_externalPluginLoaderFailed.store(false); +} + +bool didExternalPluginLoaderFail() +{ + return g_externalPluginLoaderFailed.load(); +} + +void redirectGLibLogging() +{ + g_set_print_handler(glib_print_handler); + g_set_printerr_handler(glib_printerr_handler); + g_log_set_default_handler(glib_log_handler, nullptr); +} + +void qtGstLog(GstDebugCategory *category, + GstDebugLevel level, + const gchar *file, + const gchar *function, + gint line, + GObject *object, + GstDebugMessage *message, + gpointer data) +{ + Q_UNUSED(data); + + if (level > gst_debug_category_get_threshold(category)) { + return; + } + + QMessageLogger log(file, line, function); + + struct GFree { void operator()(gchar *p) const { g_free(p); } }; + const std::unique_ptr object_info( + gst_info_strdup_printf("%" GST_PTR_FORMAT, object)); + + switch (level) { + case GST_LEVEL_ERROR: + log.critical(GStreamerAPILog, "%s %s", object_info.get(), gst_debug_message_get(message)); + break; + case GST_LEVEL_WARNING: + log.warning(GStreamerAPILog, "%s %s", object_info.get(), gst_debug_message_get(message)); + break; + case GST_LEVEL_FIXME: + case GST_LEVEL_INFO: + log.info(GStreamerAPILog, "%s %s", object_info.get(), gst_debug_message_get(message)); + break; + case GST_LEVEL_DEBUG: +#ifdef QT_DEBUG + // In release builds LOG/TRACE/MEMDUMP are intentionally dropped to reduce + // noise. Only debug builds route these verbose levels through Qt logging. + case GST_LEVEL_LOG: + case GST_LEVEL_TRACE: + case GST_LEVEL_MEMDUMP: +#endif + log.debug(GStreamerAPILog, "%s %s", object_info.get(), gst_debug_message_get(message)); + break; + default: + break; + } +} + +void configureDebugLogging() +{ + static bool s_configured = false; + if (s_configured) { + return; + } + s_configured = true; + + gst_debug_remove_log_function(gst_debug_log_default); + gst_debug_add_log_function(GStreamer::qtGstLog, nullptr, nullptr); + + if (!qEnvironmentVariableIsEmpty("GST_DEBUG")) { + return; + } + + QSettings settings; + if (settings.contains(AppSettings::gstDebugLevelName)) { + const int level = qBound(0, settings.value(AppSettings::gstDebugLevelName).toInt(), + static_cast(GST_LEVEL_MEMDUMP)); + gst_debug_set_default_threshold(static_cast(level)); + } +} + +} // namespace GStreamer diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h new file mode 100644 index 000000000000..c1c205eccf1c --- /dev/null +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +Q_DECLARE_LOGGING_CATEGORY(GStreamerLog) +Q_DECLARE_LOGGING_CATEGORY(GStreamerAPILog) +Q_DECLARE_LOGGING_CATEGORY(GStreamerDecoderRanksLog) + +namespace GStreamer +{ + void redirectGLibLogging(); + void resetExternalPluginLoaderFailure(); + bool didExternalPluginLoaderFail(); + + void configureDebugLogging(); + + void qtGstLog(GstDebugCategory *category, + GstDebugLevel level, + const gchar *file, + const gchar *function, + gint line, + GObject *object, + GstDebugMessage *message, + gpointer data); +} diff --git a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc index e23866817b64..589faaf8446f 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc @@ -24,7 +24,7 @@ GstVideoReceiver::GstVideoReceiver(QObject *parent) : VideoReceiver(parent) , _worker(new GstVideoWorker(this)) { - // qCDebug(GstVideoReceiverLog) << this; + qCDebug(GstVideoReceiverLog) << this; _worker->start(); (void) connect(&_watchdogTimer, &QTimer::timeout, this, &GstVideoReceiver::_watchdog); @@ -36,7 +36,7 @@ GstVideoReceiver::~GstVideoReceiver() stop(); _worker->shutdown(); - // qCDebug(GstVideoReceiverLog) << this; + qCDebug(GstVideoReceiverLog) << this; } void GstVideoReceiver::start(uint32_t timeout) @@ -290,7 +290,6 @@ void GstVideoReceiver::stop() GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-stopped"); gst_clear_object(&_pipeline); - _pipeline = nullptr; _recorderValve = nullptr; _decoderValve = nullptr; @@ -367,8 +366,11 @@ void GstVideoReceiver::startDecoding(void *sink) return; } + _ensureVideoSinkInPipeline(); + if (!_addDecoder(_decoderValve)) { qCCritical(GstVideoReceiverLog) << "_addDecoder() failed" << _uri; + _cleanupVideoSinkOnFailure(); _dispatchSignal([this]() { emit onStartDecodingComplete(STATUS_FAIL); }); return; } @@ -641,7 +643,7 @@ GstElement *GstVideoReceiver::_makeSource(const QString &input) do { if (isRtsp) { - if (!GStreamer::is_valid_rtsp_uri(input.toUtf8().constData())) { + if (!GStreamer::isValidRtspUri(input.toUtf8().constData())) { qCCritical(GstVideoReceiverLog) << "Invalid RTSP URI:" << input; break; } @@ -791,15 +793,12 @@ GstElement *GstVideoReceiver::_makeSource(const QString &input) return srcbin; } -GstElement *GstVideoReceiver::_makeDecoder(GstCaps *caps, GstElement *videoSink) +GstElement *GstVideoReceiver::_makeDecoder() { - Q_UNUSED(caps); Q_UNUSED(videoSink) - GstElement *decoder = gst_element_factory_make("decodebin3", nullptr); if (!decoder) { qCCritical(GstVideoReceiverLog) << "gst_element_factory_make('decodebin3') failed"; } - return decoder; } @@ -899,8 +898,11 @@ void GstVideoReceiver::_onNewSourcePad(GstPad *pad) GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-new-source-pad"); + _ensureVideoSinkInPipeline(); + if (!_addDecoder(_decoderValve)) { qCCritical(GstVideoReceiverLog) << "_addDecoder() failed"; + _cleanupVideoSinkOnFailure(); return; } @@ -928,7 +930,7 @@ void GstVideoReceiver::_logDecodebin3SelectedCodec(GstElement *decodebin3) GstPluginFeature *feature = GST_PLUGIN_FEATURE(factory); const gchar *featureName = gst_plugin_feature_get_name(feature); const guint rank = gst_plugin_feature_get_rank(feature); - bool isHardwareDecoder = GStreamer::is_hardware_decoder_factory(factory); + bool isHardwareDecoder = GStreamer::isHardwareDecoderFactory(factory); QString pluginName = featureName; GstPlugin *plugin = gst_plugin_feature_get_plugin(feature); @@ -937,6 +939,13 @@ void GstVideoReceiver::_logDecodebin3SelectedCodec(GstElement *decodebin3) gst_object_unref(plugin); } qCDebug(GstVideoReceiverLog) << "Decodebin3 selected codec:rank -" << pluginName << "/" << featureName << "-" << decoderKlass << (isHardwareDecoder ? "(HW)" : "(SW)") << ":" << rank; + + // Disable QoS on the internal decoder to prevent cascading + // frame drops on live streams. The videodecoder base class + // aggressively advances earliest_time after the first late + // frame, causing all subsequent frames to be dropped. + g_object_set(child, "qos", FALSE, nullptr); + qCDebug(GstVideoReceiverLog) << "Disabled QoS on internal decoder" << featureName; } } g_value_reset(&value); @@ -962,32 +971,14 @@ void GstVideoReceiver::_onNewDecoderPad(GstPad *pad) bool GstVideoReceiver::_addDecoder(GstElement *src) { - GstPad *srcpad = gst_element_get_static_pad(src, "src"); - if (!srcpad) { - qCCritical(GstVideoReceiverLog) << "gst_element_get_static_pad() failed"; - return false; - } - - GstCaps *caps = gst_pad_query_caps(srcpad, nullptr); - if (!caps) { - qCCritical(GstVideoReceiverLog) << "gst_pad_query_caps() failed"; - gst_clear_object(&srcpad); - return false; - } - - gst_clear_object(&srcpad); - _decoder = _makeDecoder(); if (!_decoder) { qCCritical(GstVideoReceiverLog) << "_makeDecoder() failed"; - gst_clear_caps(&caps); return false; } (void) gst_object_ref(_decoder); - gst_clear_caps(&caps); - (void) gst_bin_add(GST_BIN(_pipeline), _decoder); (void) gst_element_sync_state_with_parent(_decoder); @@ -995,6 +986,9 @@ bool GstVideoReceiver::_addDecoder(GstElement *src) if (!gst_element_link(src, _decoder)) { qCCritical(GstVideoReceiverLog) << "Unable to link decoder"; + (void) gst_element_set_state(_decoder, GST_STATE_NULL); + (void) gst_bin_remove(GST_BIN(_pipeline), _decoder); + gst_clear_object(&_decoder); return false; } @@ -1026,26 +1020,70 @@ bool GstVideoReceiver::_addDecoder(GstElement *src) return true; } +void GstVideoReceiver::_ensureVideoSinkInPipeline() +{ + if (!_videoSink || !_pipeline) { + return; + } + + GstObject *parent = gst_element_get_parent(_videoSink); + if (parent) { + gst_object_unref(parent); + return; + } + + g_object_set(_videoSink, + "widget", _widget, + "sync", (_buffer >= 0), + NULL); + + (void) gst_object_ref(_videoSink); + (void) gst_bin_add(GST_BIN(_pipeline), _videoSink); + + // PAUSED (not READY) triggers gst_gl_ensure_element_data in qml6glsink, + // which creates GstGLDisplay/GstGLContext from Qt's EGL context and posts + // HAVE_CONTEXT on the bus. Downstream GL elements and decoders acquire + // this context via the pipeline's context store. + (void) gst_element_set_state(_videoSink, GST_STATE_PAUSED); +} + bool GstVideoReceiver::_addVideoSink(GstPad *pad) { GstCaps *caps = gst_pad_query_caps(pad, nullptr); - (void) gst_object_ref(_videoSink); // gst_bin_add() will steal one reference - (void) gst_bin_add(GST_BIN(_pipeline), _videoSink); + _ensureVideoSinkInPipeline(); - if (!gst_element_link(_decoder, _videoSink)) { - (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink); - qCCritical(GstVideoReceiverLog) << "Unable to link video sink"; + GstPad *sinkPad = gst_element_get_static_pad(_videoSink, "sink"); + GstPadLinkReturn linkRet = sinkPad ? gst_pad_link(pad, sinkPad) : GST_PAD_LINK_WRONG_HIERARCHY; + if (linkRet != GST_PAD_LINK_OK) { + qCCritical(GstVideoReceiverLog) << "Unable to link decoder pad to video sink, result:" << linkRet; + + // Keep retry behavior by fully detaching the sink when link fails. + // _ensureVideoSinkInPipeline() adds it before linking. + GstObject *parent = gst_element_get_parent(_videoSink); + if (parent) { + (void) gst_element_set_state(_videoSink, GST_STATE_NULL); + (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink); + gst_clear_object(&parent); + } + + gst_clear_object(&sinkPad); gst_clear_caps(&caps); return false; } + gst_clear_object(&sinkPad); + (void) gst_element_sync_state_with_parent(_videoSink); + + // qml6glsink resets max-lateness=0 during state transitions (it renders + // on QML's paint cycle). For live streams with hardware decoders that + // have startup latency, this causes all frames to be silently dropped + // after the first one. Override after state sync so it sticks. + // Re-apply the configured sync value since state sync may also reset it. g_object_set(_videoSink, - "widget", _widget, "sync", (_buffer >= 0), - NULL); - - (void) gst_element_sync_state_with_parent(_videoSink); + "max-lateness", G_GINT64_CONSTANT(-1), + nullptr); GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-videosink"); @@ -1152,13 +1190,34 @@ bool GstVideoReceiver::_unlinkBranch(GstElement *from) return true; } +void GstVideoReceiver::_cleanupVideoSinkOnFailure() +{ + if (_videoSinkProbeId != 0) { + GstPad *sinkpad = gst_element_get_static_pad(_videoSink, "sink"); + if (sinkpad) { + gst_pad_remove_probe(sinkpad, _videoSinkProbeId); + gst_clear_object(&sinkpad); + } + _videoSinkProbeId = 0; + } + + GstObject *parent = gst_element_get_parent(_videoSink); + if (parent) { + (void) gst_element_set_state(_videoSink, GST_STATE_NULL); + (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink); + gst_clear_object(&parent); + } + + gst_clear_object(&_videoSink); +} + void GstVideoReceiver::_shutdownDecodingBranch() { if (_decoder) { GstObject *parent = gst_element_get_parent(_decoder); if (parent) { - (void) gst_bin_remove(GST_BIN(_pipeline), _decoder); (void) gst_element_set_state(_decoder, GST_STATE_NULL); + (void) gst_bin_remove(GST_BIN(_pipeline), _decoder); gst_clear_object(&parent); } @@ -1178,8 +1237,8 @@ void GstVideoReceiver::_shutdownDecodingBranch() GstObject *parent = gst_element_get_parent(_videoSink); if (parent) { - (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink); (void) gst_element_set_state(_videoSink, GST_STATE_NULL); + (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink); gst_clear_object(&parent); } @@ -1472,12 +1531,12 @@ GstPadProbeReturn GstVideoReceiver::_keyframeWatch(GstPad *pad, GstPadProbeInfo GstVideoWorker::GstVideoWorker(QObject *parent) : QThread(parent) { - // qCDebug(GstVideoReceiverLog) << this; + qCDebug(GstVideoReceiverLog) << this; } GstVideoWorker::~GstVideoWorker() { - // qCDebug(GstVideoReceiverLog) << this; + qCDebug(GstVideoReceiverLog) << this; } bool GstVideoWorker::needDispatch() const diff --git a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h index 1d65365f7dab..db9ff91a99dd 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h @@ -66,12 +66,13 @@ private slots: private: GstElement *_makeSource(const QString &input); - GstElement *_makeDecoder(GstCaps *caps = nullptr, GstElement *videoSink = nullptr); + GstElement *_makeDecoder(); GstElement *_makeFileSink(const QString &videoFile, FILE_FORMAT format); void _onNewSourcePad(GstPad *pad); void _onNewDecoderPad(GstPad *pad); bool _addDecoder(GstElement *src); + void _ensureVideoSinkInPipeline(); bool _addVideoSink(GstPad *pad); void _noteTeeFrame(); void _noteVideoSinkFrame(); @@ -79,6 +80,7 @@ private slots: /// -Unlink the branch from the src pad /// -Send an EOS event at the beginning of that branch bool _unlinkBranch(GstElement *from); + void _cleanupVideoSinkOnFailure(); void _shutdownDecodingBranch(); void _shutdownRecordingBranch(); void _logDecodebin3SelectedCodec(GstElement *decodebin3); diff --git a/src/VideoManager/VideoReceiver/GStreamer/README.md b/src/VideoManager/VideoReceiver/GStreamer/README.md index a0aa741de3ff..70e0405043ea 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/README.md +++ b/src/VideoManager/VideoReceiver/GStreamer/README.md @@ -1,158 +1,64 @@ -# QGroundControl +# GStreamer Video Streaming -## Video Streaming +QGroundControl uses GStreamer for UDP RTP and RTSP video streaming in the Main Flight Display. -QGroundControl implements an UDP RTP and RSTP video streaming receiver in its Main Flight Display using GStreamer. -Current suggested version of GStreamer is **1.22.12** - other versions of GStreamer may break the build as some dependent libraries may change. -To build video streaming support, you will need to install the GStreamer development packages for the desired target platform. +## Build Configuration -If you do have the proper GStreamer development libraries installed where QGC looks for it, the QGC build system will automatically use it and build video streaming support. If you would like to disable GStreamer video streaming support, set the **QGC_ENABLE_GST_VIDEOSTREAMING** CMake option to **OFF**. +- **Enable/disable**: Set `QGC_ENABLE_GST_VIDEOSTREAMING` CMake option to `ON`/`OFF` +- **Version & URLs**: Defined in [`.github/build-config.json`](../../../../.github/build-config.json), parsed by [`cmake/BuildConfig.cmake`](../../../../cmake/BuildConfig.cmake) +- **SDK discovery & auto-download**: [`cmake/find-modules/FindQGCGStreamer.cmake`](../../../../cmake/find-modules/FindQGCGStreamer.cmake) +- **Plugin allowlist**: `GSTREAMER_PLUGINS` in `FindQGCGStreamer.cmake` — controls both static linking (mobile) and dynamic install (desktop) +- **Install helpers**: [`cmake/find-modules/GStreamerHelpers.cmake`](../../../../cmake/find-modules/GStreamerHelpers.cmake) -### Gstreamer logs +## Runtime Environment Setup -For cases, when it is need to have more control over gstreamer logging than is availabe via QGroundControl's UI, it is possible to configure gstreamer logging via environment variables. Please see for details. +QGC configures GStreamer runtime paths before `gst_init()`: -### UDP Pipeline +- **Desktop (Linux/macOS/Windows)**: `GStreamer::prepareEnvironment()` sets plugin paths, scanner/helper paths, and GIO module paths when bundled runtimes are detected. +- **Linux AppImage**: [`deploy/linux/AppRun`](../../../../deploy/linux/AppRun) exports `GST_PLUGIN_*`, `GST_PLUGIN_SCANNER*`, and `GST_PTP_HELPER*` only when bundled paths are valid. +- **Environment hygiene**: Python virtualenv/conda variables are temporarily cleared during `gst_init()` for scanner stability and restored afterwards. +- **Validation**: If bundled plugin directories are present but `gst-plugin-scanner` is missing or non-executable, a warning is logged and GStreamer falls back to in-process plugin scanning. -For the time being, the RTP UDP pipeline is somewhat hardcoded, using h.264 or h.265. It's best to use a camera capable of hardware encoding either h.264 (such as the Logitech C920) or h.265. On the sender end, for RTP (UDP Streaming) you would run something like this: +## Platform Setup -h.264 - -``` -gst-launch-1.0 uvch264src initial-bitrate=1000000 average-bitrate=1000000 iframe-period=1000 device=/dev/video0 name=src auto-start=true src.vidsrc ! video/x-h264,width=1920,height=1080,framerate=24/1 ! h264parse ! rtph264pay ! udpsink host=xxx.xxx.xxx.xxx port=5600 -``` - -h.265 - -``` -ffmpeg -f v4l2 -i /dev/video1 -pix_fmt yuv420p -c:v libx265 -preset ultrafast -x265-params crf=23 -strict experimental -f rtp udp://xxx.xxx.xxx.xxx:5600 -``` - -Where xxx.xxx.xxx.xxx is the IP address where QGC is running. - -To test using a test source on localhost, you can run this command: - -``` -gst-launch-1.0 videotestsrc pattern=ball ! video/x-raw,width=640,height=480 ! x264enc ! rtph264pay ! udpsink host=127.0.0.1 port=5600 -``` - -Or this one: - -``` -gst-launch-1.0 videotestsrc ! video/x-raw,width=640,height=480 ! videoconvert ! x264enc ! rtph264pay ! udpsink host=127.0.0.1 port=5600 -``` - -On the receiving end, if you want to test it from the command line, you can use something like: - -``` -gst-launch-1.0 udpsrc port=5600 caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264' ! rtpjitterbuffer ! rtph264depay ! h264parse ! avdec_h264 ! autovideosink fps-update-interval=1000 sync=false -``` - -Or this one: - -``` -gst-launch-1.0 udpsrc port=5600 ! application/x-rtp ! rtpjitterbuffer ! rtph264depay ! avdec_h264 ! videoconvert ! autovideosink -``` +### Linux -Or this one, note that removing rtpjitterbuffer would reduce video latency as low latency mode is doing: +Use `tools/setup/install-dependencies-debian.sh`, or manually: ``` -gst-launch-1.0 udpsrc port=5600 caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264' ! rtpjitterbuffer ! parsebin ! decodebin ! autovideosink fps-update-interval=1000 sync=false +sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev python3-gi python3-gst-1.0 ``` -### Additional Protocols - -QGC also supports RTSP, TCP-MPEG2 and MPEG-TS pipelines. +### macOS / Windows / iOS -## Setup +GStreamer SDKs are auto-downloaded during CMake configure. To use a local installation instead, set `GStreamer_ROOT_DIR`. -### Linux +### Android -For a quick setup, use `python3 qgroundcontrol/tools/setup/install_dependencies.py --platform debian`. +Auto-downloaded during CMake configure. No manual setup required. -Alternatively for a manual approach you can use apt-get to install GStreamer 1.0: +> **Windows users building for Android**: Enable Developer Mode (Settings > System > For developers) to support symbolic links during the build. -``` -list=$(apt-cache --names-only search ^gstreamer1.0-* | awk '{ print $1 }' | sed -e /-doc/d | grep -v gstreamer1.0-hybris) -``` +## Testing Pipelines -``` -sudo apt-get install $list -``` +### Sending test video (h.264 over UDP) ``` -sudo apt-get install libgstreamer-plugins-base1.0-dev -sudo apt-get install libgstreamer-plugins-bad1.0-dev +gst-launch-1.0 videotestsrc pattern=ball ! video/x-raw,width=640,height=480 ! x264enc ! rtph264pay ! udpsink host=127.0.0.1 port=5600 ``` -The build system is setup to use pkgconfig and it will find the necessary headers and libraries automatically. - -### Mac OS - -Download the gstreamer framework from here: . Supported version is 1.22.12. - -You need two packages: - -- [gstreamer-1.0-devel-1.22.12-x86_64.pkg](https://gstreamer.freedesktop.org/data/pkg/osx/1.22.12/gstreamer-1.0-devel-1.22.12-universal.pkg) -- [gstreamer-1.0-1.22.12-x86_64.pkg](https://gstreamer.freedesktop.org/data/pkg/osx/1.22.12/gstreamer-1.0-1.22.12-universal.pkg) - -The installer places them under /Library/Frameworks/GStreamer.framework, which is where the QGC build system will look for it. That's all that is needed. When you build QGC and it finds the gstreamer framework, it automatically builds video streaming support. - -:point_right: To run gstreamer commands from the command line, you can add the path to find them (either in ~/.profile or ~/.bashrc): +### Receiving test video ``` -export PATH=$PATH:/Library/Frameworks/GStreamer.framework/Commands +gst-launch-1.0 udpsrc port=5600 caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264' ! rtpjitterbuffer ! rtph264depay ! h264parse ! avdec_h264 ! autovideosink sync=false ``` -### iOS - -Download the gstreamer framework from here: [gstreamer-1.0-devel-1.22.12-ios-universal.pkg](https://gstreamer.freedesktop.org/data/pkg/ios/1.22.12/gstreamer-1.0-devel-1.22.12-ios-universal.pkg) - -The installer places them under ~/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework, which is where the QGC build system will look for it. That's all that is needed. When you build QGC and it finds the gstreamer framework, it automatically builds video streaming support. - -### Android - -An appropriate version of GStreamer will be automatically downloaded as part of the CMake configure step. - -#### Important Note for Windows Users - -During the build process for Android on Windows, to ensure support for UNIX-like symbolic links when not using elevated privileges, **Developer Mode** must be enabled. Without enabling Developer Mode, you may encounter linking issues with the automatically downloaded prebuilt version of GStreamer. - -To enable Developer Mode: - -1. Open the **Settings** app on Windows. -2. Go to **System** > **For developers**. -3. Enable **Developer Mode** by toggling the switch. - -### Windows - -Download the gstreamer framework from here: . Supported version is 1.22.12. QGC may work with newer version, but it is untested. - -You need two packages: - -- [gstreamer-1.0-devel-msvc-x86_64-1.22.12.msi](https://gstreamer.freedesktop.org/data/pkg/windows/1.22.12/msvc/gstreamer-1.0-devel-msvc-x86_64-1.22.12.msi) -- [gstreamer-1.0-msvc-x86_64-1.22.12.msi](https://gstreamer.freedesktop.org/data/pkg/windows/1.22.12/msvc/gstreamer-1.0-msvc-x86_64-1.22.12.msi) - -Make sure you select "Complete" installation instead of "Typical" installation during the install process. +### Supported protocols -The following environment variables can be used to configure the GStreamer installation path: -GSTREAMER_1_0_ROOT_X86_64 -GSTREAMER_1_0_ROOT_MSVC_X86_64 -GSTREAMER_1_0_ROOT_MINGW_X86_64 +UDP RTP, RTSP, TCP-MPEG2, MPEG-TS. -### Gstreamer Config +## GStreamer Logging -When running QGC, you can provide the command line options to configure GStreamer: ---gst-version "Print the GStreamer version" ---gst-fatal-warnings "Make all warnings fatal" ---gst-debug-help "Print available debug categories and exit" ---gst-debug-level "Default debug level from 1 (only error) to 9 (anything) or 0 for no output" ---gst-debug "Comma-separated list of category_name:level pairs to set specific levels for the individual categories. Example: GST_AUTOPLUG:5,GST_ELEMENT_*:3" ---gst-debug-no-color "Disable colored debugging output" ---gst-debug-color-mode "Changes coloring mode of the debug log. Possible modes: off, on, disable, auto, unix" ---gst-debug-disable "Disable debugging" ---gst-plugin-path "Colon-separated paths containing plugins" ---gst-plugin-load "Comma-separated list of plugins to preload in addition to the list stored in environment variable GST_PLUGIN_PATH" ---gst-disable-segtrap "Disable trapping of segmentation faults during plugin loading" ---gst-disable-registry-update "Disable updating the registry" ---gst-disable-registry-fork "Disable spawning a helper process while scanning the registry" +- In-app: Use QGroundControl's logging settings +- Environment variables: See +- Command line options: `--gst-debug-level`, `--gst-debug`, `--gst-debug-help`, etc. diff --git a/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc b/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc index 8f4b5d269c5c..7b93bc27fa30 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc @@ -11,6 +11,7 @@ GST_DEBUG_CATEGORY_STATIC(GST_CAT_DEFAULT); #define DEFAULT_PAR_N 0 #define DEFAULT_PAR_D 1 #define DEFAULT_SYNC TRUE +#define DEFAULT_MAX_LATENESS G_GINT64_CONSTANT(-1) #define PROP_ENABLE_LAST_SAMPLE_NAME "enable-last-sample" #define PROP_LAST_SAMPLE_NAME "last-sample" @@ -18,6 +19,7 @@ GST_DEBUG_CATEGORY_STATIC(GST_CAT_DEFAULT); #define PROP_FORCE_ASPECT_RATIO_NAME "force-aspect-ratio" #define PROP_PIXEL_ASPECT_RATIO_NAME "pixel-aspect-ratio" #define PROP_SYNC_NAME "sync" +#define PROP_MAX_LATENESS_NAME "max-lateness" enum { @@ -28,6 +30,7 @@ enum PROP_FORCE_ASPECT_RATIO, PROP_PIXEL_ASPECT_RATIO, PROP_SYNC, + PROP_MAX_LATENESS, PROP_LAST }; @@ -118,6 +121,14 @@ gst_qgc_video_sink_bin_class_init(GstQgcVideoSinkBinClass *klass) (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS) ); + properties[PROP_MAX_LATENESS] = g_param_spec_int64( + "max-lateness", "Max lateness", + "Maximum number of nanoseconds a buffer can be late before it is dropped (-1 unlimited)", + -1, G_MAXINT64, + DEFAULT_MAX_LATENESS, + (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS) + ); + g_object_class_install_properties(object_class, PROP_LAST, properties); gst_qgc_video_sink_bin_signals[SIGNAL_CREATE_ELEMENT] = g_signal_new( @@ -194,38 +205,50 @@ gst_qgc_video_sink_bin_set_property(GObject *object, guint prop_id, const GValue switch (prop_id) { case PROP_ENABLE_LAST_SAMPLE: - g_object_set(self->glsinkbin, - PROP_ENABLE_LAST_SAMPLE_NAME, - g_value_get_boolean(value), - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_set(self->glsinkbin, + PROP_ENABLE_LAST_SAMPLE_NAME, + g_value_get_boolean(value), + NULL); break; case PROP_WIDGET: - g_object_set(self->qmlglsink, - PROP_WIDGET_NAME, - g_value_get_pointer(value), - NULL); + if (G_LIKELY(self->qmlglsink)) + g_object_set(self->qmlglsink, + PROP_WIDGET_NAME, + g_value_get_pointer(value), + NULL); break; case PROP_FORCE_ASPECT_RATIO: - g_object_set(self->glsinkbin, - PROP_FORCE_ASPECT_RATIO_NAME, - g_value_get_boolean(value), - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_set(self->glsinkbin, + PROP_FORCE_ASPECT_RATIO_NAME, + g_value_get_boolean(value), + NULL); break; case PROP_PIXEL_ASPECT_RATIO: { const gint num = gst_value_get_fraction_numerator(value); const gint den = gst_value_get_fraction_denominator(value); - g_object_set(self->qmlglsink, - PROP_PIXEL_ASPECT_RATIO_NAME, - num, - den, - NULL); + if (G_LIKELY(self->qmlglsink)) + g_object_set(self->qmlglsink, + PROP_PIXEL_ASPECT_RATIO_NAME, + num, + den, + NULL); break; } case PROP_SYNC: - g_object_set(self->glsinkbin, - PROP_SYNC_NAME, - g_value_get_boolean(value), - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_set(self->glsinkbin, + PROP_SYNC_NAME, + g_value_get_boolean(value), + NULL); + break; + case PROP_MAX_LATENESS: + if (G_LIKELY(self->qmlglsink)) + g_object_set(self->qmlglsink, + PROP_MAX_LATENESS_NAME, + g_value_get_int64(value), + NULL); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); @@ -241,19 +264,21 @@ gst_qgc_video_sink_bin_get_property(GObject *object, guint prop_id, GValue *valu switch (prop_id) { case PROP_ENABLE_LAST_SAMPLE: { gboolean enable = FALSE; - g_object_get(self->glsinkbin, - PROP_ENABLE_LAST_SAMPLE_NAME, - &enable, - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_get(self->glsinkbin, + PROP_ENABLE_LAST_SAMPLE_NAME, + &enable, + NULL); g_value_set_boolean(value, enable); break; } case PROP_LAST_SAMPLE: { GstSample *sample = NULL; - g_object_get(self->glsinkbin, - PROP_LAST_SAMPLE_NAME, - &sample, - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_get(self->glsinkbin, + PROP_LAST_SAMPLE_NAME, + &sample, + NULL); gst_value_set_sample(value, sample); if (sample) { gst_sample_unref(sample); @@ -262,40 +287,54 @@ gst_qgc_video_sink_bin_get_property(GObject *object, guint prop_id, GValue *valu } case PROP_WIDGET: { gpointer widget = NULL; - g_object_get(self->qmlglsink, - PROP_WIDGET_NAME, - &widget, - NULL); + if (G_LIKELY(self->qmlglsink)) + g_object_get(self->qmlglsink, + PROP_WIDGET_NAME, + &widget, + NULL); g_value_set_pointer(value, widget); break; } case PROP_FORCE_ASPECT_RATIO: { gboolean enable = FALSE; - g_object_get(self->glsinkbin, - PROP_FORCE_ASPECT_RATIO_NAME, - &enable, - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_get(self->glsinkbin, + PROP_FORCE_ASPECT_RATIO_NAME, + &enable, + NULL); g_value_set_boolean(value, enable); break; } case PROP_PIXEL_ASPECT_RATIO: { gint num = 0, den = 1; - g_object_get(self->qmlglsink, - PROP_PIXEL_ASPECT_RATIO_NAME, - &num, &den, - NULL); + if (G_LIKELY(self->qmlglsink)) + g_object_get(self->qmlglsink, + PROP_PIXEL_ASPECT_RATIO_NAME, + &num, &den, + NULL); gst_value_set_fraction(value, num, den); break; } case PROP_SYNC: { gboolean enable = FALSE; - g_object_get(self->glsinkbin, - PROP_SYNC_NAME, - &enable, - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_get(self->glsinkbin, + PROP_SYNC_NAME, + &enable, + NULL); g_value_set_boolean(value, enable); break; } + case PROP_MAX_LATENESS: { + gint64 lateness = DEFAULT_MAX_LATENESS; + if (G_LIKELY(self->qmlglsink)) + g_object_get(self->qmlglsink, + PROP_MAX_LATENESS_NAME, + &lateness, + NULL); + g_value_set_int64(value, lateness); + break; + } default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; diff --git a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt b/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt index 6e75ca44c6c8..78e41bf3df54 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt +++ b/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt @@ -12,16 +12,11 @@ target_link_libraries(gstqml6gl PUBLIC GStreamer::GStreamer) # return() # endif() -# ============================================================================ -# GStreamer Good Plugins Integration -# ============================================================================ - include(GStreamerHelpers) message(STATUS "gstqml6gl: GStreamer_VERSION = '${GStreamer_VERSION}'") if(GStreamer_VERSION VERSION_GREATER_EQUAL "1.22") - # Get recommended stable version for this GStreamer release string(REPLACE "." ";" GST_VERSION_LIST "${GStreamer_VERSION}") list(GET GST_VERSION_LIST 0 GST_VERSION_MAJOR) list(GET GST_VERSION_LIST 1 GST_VERSION_MINOR) @@ -30,7 +25,6 @@ if(GStreamer_VERSION VERSION_GREATER_EQUAL "1.22") set(GST_VERSION_PATCH "${CMAKE_MATCH_1}") endif() - # Use centralized version mapping from GStreamerHelpers.cmake gstreamer_get_recommended_version( ${GST_VERSION_MAJOR} ${GST_VERSION_MINOR} @@ -44,8 +38,6 @@ if(GStreamer_VERSION VERSION_GREATER_EQUAL "1.22") gstreamer_get_package_url(good_plugins ${GST_PLUGINS_VERSION} _gst_good_url) gstreamer_get_package_url(good_plugins_qt6 ${GST_PLUGINS_VERSION} _gst_qt6_url) gstreamer_fetch_checksum(good_plugins ${GST_PLUGINS_VERSION} _gst_good_hash) - _gstreamer_is_release_like_config(_gst_release_like) - set(_gst_qt6_cache "${CMAKE_BINARY_DIR}/_deps/gstreamer-good-plugins") gstreamer_resilient_download( URLS "${_gst_good_url}" @@ -57,11 +49,6 @@ if(GStreamer_VERSION VERSION_GREATER_EQUAL "1.22") ) if(NOT _gst_good_archive) - if(_gst_release_like) - message(FATAL_ERROR - "gstqml6gl: Primary gst-plugins-good source download failed.\n" - "Release-like builds do not allow the unverified GitLab qt6 fallback archive.") - endif() message(STATUS "gstqml6gl: Primary source tarball unavailable, trying GitLab qt6 fallback archive") gstreamer_resilient_download( URLS "${_gst_qt6_url}" @@ -108,10 +95,6 @@ file(REMOVE_RECURSE "${_qt6_build_sources}") file(COPY "${QGC_GST_QT6_PLUGIN_PATH}/" DESTINATION "${_qt6_build_sources}") set(QGC_GST_QT6_PLUGIN_PATH "${_qt6_build_sources}") -# ============================================================================ -# Source Code Patching -# ============================================================================ - # Upstream qt6glitem.h can miss QQuickWindow include, which breaks moc-generated # metatype code with newer Qt. Patch the copied source tree only. set(_qt6glitem_header "${QGC_GST_QT6_PLUGIN_PATH}/qt6glitem.h") @@ -136,7 +119,6 @@ file(GLOB gstqml6gl_SRCS ) target_sources(gstqml6gl PRIVATE ${gstqml6gl_SRCS}) -# Suppress compiler warnings for third-party GStreamer plugin sources qgc_disable_dependency_warnings(gstqml6gl) target_link_libraries(gstqml6gl @@ -158,11 +140,6 @@ target_precompile_headers(gstqml6gl ) -# ============================================================================ -# Shader Compilation -# ============================================================================ - -# Discover shader files dynamically to support different GStreamer versions file(GLOB _shader_files "${QGC_GST_QT6_PLUGIN_PATH}/*.vert" "${QGC_GST_QT6_PLUGIN_PATH}/*.frag" @@ -180,7 +157,6 @@ if(_shader_files) list(APPEND OUTPUTS "${_name}.qsb") endforeach() - # Compile shaders for different GLSL versions qt_add_shaders(gstqml6gl "gstqml6gl_shaders" PREFIX "/org/freedesktop/gstreamer/qml6" GLSL "100 es,120,330" @@ -190,8 +166,7 @@ if(_shader_files) BATCHABLE ) - # Handle additional GLES-specific RGBA shader if present - # This generates RGBA.frag.qsb.external which adds GLES 100 support to the base shader + # RGBA_gles.frag generates RGBA.frag.qsb.external (GLES 100 support for the base shader) if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/RGBA_gles.frag") find_package(Python3 REQUIRED COMPONENTS Interpreter) @@ -208,12 +183,10 @@ if(_shader_files) message(FATAL_ERROR "qsb-wrapper.py not found at ${QSB_WRAPPER}") endif() - # The base shader is generated by qt_add_shaders into the .qsb directory set(RGBA_BASE_QSB "${CMAKE_CURRENT_BINARY_DIR}/.qsb/RGBA.frag.qsb") set(RGBA_GLES_SRC "${QGC_GST_QT6_PLUGIN_PATH}/RGBA_gles.frag") set(RGBA_EXTERNAL "${CMAKE_CURRENT_BINARY_DIR}/.qsb/RGBA.frag.qsb.external") - # Ensure the .qsb directory exists file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.qsb") add_custom_command( @@ -232,8 +205,6 @@ if(_shader_files) VERBATIM ) - # Create a target for the external shader to ensure proper dependency ordering - # The custom command depends on RGBA_BASE_QSB file, which is generated by gstqml6gl_shaders target add_custom_target(gstqml6gl_external_shader DEPENDS ${RGBA_EXTERNAL}) if(gstqml6gl_shaders) add_dependencies(gstqml6gl_external_shader ${gstqml6gl_shaders}) @@ -245,15 +216,10 @@ if(_shader_files) FILES "${RGBA_EXTERNAL}" ) - # Ensure the resource target depends on the external shader being generated add_dependencies(gstqml6gl gstqml6gl_external_shader) endif() endif() -# ============================================================================ -# Platform-Specific OpenGL Support -# ============================================================================ - if(GStreamer_GlX11_FOUND) target_compile_definitions(gstqml6gl PRIVATE HAVE_QT_X11) endif() diff --git a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/qt6/meson.build b/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/qt6/meson.build deleted file mode 100644 index 150b0b9f222f..000000000000 --- a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/qt6/meson.build +++ /dev/null @@ -1,145 +0,0 @@ -sources = [ - 'gstplugin.cc', - 'gstqt6element.cc', - 'gstqsg6glnode.cc', - 'gstqt6glutility.cc', - 'gstqml6glsink.cc', - 'qt6glitem.cc', -] - -moc_headers = [ - 'qt6glitem.h', - 'gstqsg6glnode.h', -] - -qt6qml_dep = dependency('', required: false) -qt6_option = get_option('qt6') -qt6_method = get_option('qt-method') - -if qt6_option.disabled() - subdir_done() -endif - -if not have_gstgl - if qt6_option.enabled() - error('qt6 qmlglsink plugin is enabled, but gstreamer-gl-1.0 was not found') - endif - subdir_done() -endif - -if not add_languages('cpp', native: false, required: qt6_option) - subdir_done() -endif - -qt6_mod = import('qt6') -if not qt6_mod.has_tools() - if qt6_option.enabled() - error('qt6 qmlglsink plugin is enabled, but qt specific tools were not found') - endif - subdir_done() -endif - -qt6qml_dep = dependency('qt6', modules : ['Core', 'Gui', 'Qml', 'Quick'], - method: qt6_method, required: qt6_option, static: host_system == 'ios') -if not qt6qml_dep.found() - subdir_done() -endif - -optional_deps = [] -qt_defines = [] -have_qpa_include = false -have_qt_windowing = false - -# Look for the QPA platform native interface header -qpa_header_path = join_paths(qt6qml_dep.version(), 'QtGui') -qpa_header = join_paths(qpa_header_path, 'qpa/qplatformnativeinterface.h') -if cxx.has_header(qpa_header, dependencies : qt6qml_dep) - qt_defines += '-DHAVE_QT_QPA_HEADER' - qt_defines += '-DQT_QPA_HEADER=' + '<@0@>'.format(qpa_header) - have_qpa_include = true - message('Found QtGui QPA header in ' + qpa_header_path) -endif - -# Try to come up with all the platform/winsys combinations that will work - -if gst_gl_have_window_x11 and gst_gl_have_platform_glx - # FIXME: automagic - qt_defines += ['-DHAVE_QT_X11'] - have_qt_windowing = true -endif - -if gst_gl_have_platform_egl - # Embedded linux (e.g. i.MX6) with or without windowing support - qt_defines += ['-DHAVE_QT_EGLFS'] - optional_deps += gstglegl_dep - have_qt_windowing = true - if have_qpa_include - # Wayland windowing - if gst_gl_have_window_wayland - # FIXME: automagic - qt6waylandextras = dependency('qt6', modules : ['WaylandClient'], method: qt6_method, required : false) - if qt6waylandextras.found() - optional_deps += [qt6waylandextras, gstglwayland_dep] - qt_defines += ['-DHAVE_QT_WAYLAND'] - have_qt_windowing = true - endif - endif - # Android windowing -# if gst_gl_have_window_android - # FIXME: automagic -# qt5androidextras = dependency('qt5', modules : ['AndroidExtras'], method: qt6_method, required : false) - # for gl functions in QtGui/qopenglfunctions.h - # FIXME: automagic -# glesv2_dep = cc.find_library('GLESv2', required : false) -# if glesv2_dep.found() and qt5androidextras.found() -# optional_deps += [qt5androidextras, glesv2_dep] -# qt_defines += ['-DHAVE_QT_ANDROID'] -# have_qt_windowing = true - # Needed for C++11 support in Cerbero. People building with Android - # in some other way need to add the necessary bits themselves. -# optional_deps += dependency('gnustl', required : false) -# endif -# endif - endif -endif - -#if gst_gl_have_platform_wgl and gst_gl_have_window_win32 - # for wglMakeCurrent() - # FIXME: automagic -# opengl32_dep = cc.find_library('opengl32', required : false) -# if opengl32_dep.found() -# qt_defines += ['-DHAVE_QT_WIN32'] -# optional_deps += opengl32_dep -# have_qt_windowing = true -# endif -#endif - -if gst_gl_have_window_cocoa and gst_gl_have_platform_cgl - # FIXME: automagic - if host_machine.system() == 'darwin' - qt_defines += ['-DHAVE_QT_MAC'] - have_qt_windowing = true - endif -endif - -if gst_gl_have_window_eagl and gst_gl_have_platform_eagl - if host_machine.system() == 'ios' - qt_defines += ['-DHAVE_QT_IOS'] - have_qt_windowing = true - endif -endif - -if have_qt_windowing - # Build it! - moc_files = qt6_mod.preprocess(moc_headers : moc_headers) - gstqml6gl = library('gstqml6', sources, moc_files, - cpp_args : gst_plugins_good_args + qt_defines, - link_args : noseh_link_args, - include_directories: [configinc, libsinc], - dependencies : [gst_dep, gstvideo_dep, gstgl_dep, gstglproto_dep, qt6qml_dep, optional_deps], - override_options : ['cpp_std=c++17'], - install: true, - install_dir : plugins_install_dir) - pkgconfig.generate(gstqml6gl, install_dir : plugins_pkgconfig_install_dir) - plugins += [gstqml6gl] -endif diff --git a/tools/README.md b/tools/README.md index 640d0315a437..8d38dc5526e3 100644 --- a/tools/README.md +++ b/tools/README.md @@ -208,7 +208,7 @@ python .\tools\setup\install_dependencies.py --platform windows # Install Python tooling (pre-commit, test, coverage, etc.) python3 ./tools/setup/install_python.py precommit,test,coverage -# Build GStreamer from source (Linux, optional) +# Build GStreamer from source (optional — CMake auto-downloads pre-built SDKs) python3 ./tools/setup/build-gstreamer.py --platform linux --prefix /opt/gstreamer # Read build config values @@ -441,7 +441,7 @@ Version numbers and build settings are centralized in `.github/build-config.json { "qt_version": "6.10.1", "qt_modules": "qtcharts qtlocation ...", - "gstreamer_version": "1.24.12", + "gstreamer_default_version": "1.24.13", "ndk_version": "r27c", ... } diff --git a/tools/common/build_config.py b/tools/common/build_config.py index da2a8df700a4..312a91d60047 100644 --- a/tools/common/build_config.py +++ b/tools/common/build_config.py @@ -14,7 +14,7 @@ "qt_minimum_version", "qt_modules", "gstreamer_minimum_version", - "gstreamer_version", + "gstreamer_default_version", "gstreamer_macos_version", "gstreamer_android_version", "gstreamer_windows_version", @@ -98,6 +98,11 @@ def derive_ios_qt_modules(qt_modules: str) -> str: return " ".join(modules) +_EXPORT_KEY_ALIASES: dict[str, str] = { + "gstreamer_default_version": "GSTREAMER_VERSION", +} + + def export_build_config_values( config: dict[str, Any], *, @@ -107,7 +112,8 @@ def export_build_config_values( exported: dict[str, str] = {} for key in keys or DEFAULT_EXPORT_KEYS: if key in config: - exported[key.upper()] = str(config[key]) + export_name = _EXPORT_KEY_ALIASES.get(key, key.upper()) + exported[export_name] = str(config[key]) return exported diff --git a/tools/setup/read_config.py b/tools/setup/read_config.py index 244e658ae269..5d8315199ad7 100755 --- a/tools/setup/read_config.py +++ b/tools/setup/read_config.py @@ -124,6 +124,10 @@ def main() -> int: if key in config: print(config[key]) return 0 + # Legacy alias: gstreamer_version -> gstreamer_default_version + if key == "gstreamer_version" and "gstreamer_default_version" in config: + print(config["gstreamer_default_version"]) + return 0 else: print(f"Error: Key '{key}' not found in config", file=sys.stderr) print(f"Available keys: {', '.join(sorted(config.keys()))}", file=sys.stderr) diff --git a/tools/tests/test_read_config.py b/tools/tests/test_read_config.py index 7463bf986811..5668f6d92eee 100644 --- a/tools/tests/test_read_config.py +++ b/tools/tests/test_read_config.py @@ -32,6 +32,7 @@ def _write_config(path: Path) -> None: "qt_version": "6.10.2", "qt_minimum_version": "6.8.0", "qt_modules": "qtpositioning qtserialport qtscxml", + "gstreamer_default_version": "1.28.0", "gstreamer_windows_version": "1.26.6", "android_platform": "35", } @@ -58,6 +59,16 @@ def test_missing_key_returns_error(tmp_path: Path) -> None: assert "not found" in result.stderr +def test_legacy_gstreamer_version_alias_returns_default_version(tmp_path: Path) -> None: + config = tmp_path / "build-config.json" + config.write_text(json.dumps({"gstreamer_default_version": "1.28.0"}), encoding="utf-8") + + result = _run_read_config("--get", "gstreamer_version", env={"CONFIG_FILE": str(config)}) + + assert result.returncode == 0 + assert result.stdout.strip() == "1.28.0" + + def test_export_bash_format(tmp_path: Path) -> None: config = tmp_path / "build-config.json" _write_config(config) @@ -67,6 +78,7 @@ def test_export_bash_format(tmp_path: Path) -> None: assert result.returncode == 0 assert 'export QT_VERSION="6.10.2"' in result.stdout assert 'export QT_MODULES="qtpositioning qtserialport qtscxml"' in result.stdout + assert 'export GSTREAMER_VERSION="1.28.0"' in result.stdout def test_export_bash_preserves_bang_character(tmp_path: Path) -> None: @@ -101,6 +113,7 @@ def test_github_output_includes_ios_modules(tmp_path: Path) -> None: output_text = github_output.read_text(encoding="utf-8") assert "qt_version=6.10.2" in output_text assert "qt_minimum_version=6.8.0" in output_text + assert "gstreamer_version=1.28.0" in output_text assert "gstreamer_windows_version=1.26.6" in output_text # Derived value excludes qtserialport and qtscxml and normalizes spacing. assert "qt_modules_ios=qtpositioning" in output_text From f9ea96ec4fdce99dc73089f491a5224765d9896f Mon Sep 17 00:00:00 2001 From: Sam Gillam <46759700+gillamkid@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:40:19 -0600 Subject: [PATCH 074/119] Usb video fixes (#13961) --- src/FlyView/FlightDisplayViewUVC.qml | 2 + src/FlyView/FlightDisplayViewVideo.qml | 63 +++++++++++++++++--------- src/FlyView/FlyViewVideo.qml | 9 +--- 3 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/FlyView/FlightDisplayViewUVC.qml b/src/FlyView/FlightDisplayViewUVC.qml index dbb2812a5a4b..3b78b99445de 100644 --- a/src/FlyView/FlightDisplayViewUVC.qml +++ b/src/FlyView/FlightDisplayViewUVC.qml @@ -7,6 +7,8 @@ Rectangle { id: _root width: parent.width height: parent.height + implicitWidth: videoOutput.implicitWidth + implicitHeight: videoOutput.implicitHeight color: Qt.rgba(0,0,0,0.75) clip: true anchors.centerIn: parent diff --git a/src/FlyView/FlightDisplayViewVideo.qml b/src/FlyView/FlightDisplayViewVideo.qml index f9efb19c385f..a517434d8ea8 100644 --- a/src/FlyView/FlightDisplayViewVideo.qml +++ b/src/FlyView/FlightDisplayViewVideo.qml @@ -12,9 +12,11 @@ Item { property bool useSmallFont: true - property double _ar: QGroundControl.videoManager.gstreamerEnabled - ? QGroundControl.videoManager.videoSize.width / QGroundControl.videoManager.videoSize.height - : QGroundControl.videoManager.aspectRatio + property double _ar: (cameraLoader.visible && cameraLoader.status === Loader.Ready) + ? cameraLoader.item.implicitWidth / cameraLoader.item.implicitHeight + : QGroundControl.videoManager.gstreamerEnabled + ? QGroundControl.videoManager.videoSize.width / QGroundControl.videoManager.videoSize.height + : QGroundControl.videoManager.aspectRatio property bool _showGrid: QGroundControl.settingsManager.videoSettings.gridLines.rawValue property var _dynamicCameras: globals.activeVehicle ? globals.activeVehicle.cameraManager : null property bool _connected: globals.activeVehicle ? !globals.activeVehicle.communicationLost : false @@ -23,6 +25,8 @@ Item { property var _camera: _isCamera ? _dynamicCameras.cameras.get(_curCameraIndex) : null property bool _hasZoom: _camera && _camera.hasZoom property int _fitMode: QGroundControl.settingsManager.videoSettings.videoFit.rawValue + property bool _showStreamLoader: QGroundControl.videoManager.decoding + property bool _showUvcLoader: QGroundControl.videoManager.isUvc property bool _isMode_FIT_WIDTH: _fitMode === 0 property bool _isMode_FIT_HEIGHT: _fitMode === 1 @@ -43,7 +47,7 @@ Item { anchors.fill: parent source: "/res/NoVideoBackground.jpg" fillMode: Image.PreserveAspectCrop - visible: !(QGroundControl.videoManager.decoding) + visible: !_showStreamLoader && !_showUvcLoader Rectangle { anchors.centerIn: parent @@ -68,7 +72,7 @@ Item { id: videoBackground anchors.fill: parent color: "black" - visible: QGroundControl.videoManager.decoding + visible: _showStreamLoader || _showUvcLoader function getWidth() { if(_ar != 0.0){ if(_isMode_FIT_HEIGHT @@ -116,48 +120,65 @@ Item { } } + } + } + Loader { + // GStreamer is causing crashes on Lenovo laptop OpenGL Intel drivers. In order to workaround this + // we don't load a QGCVideoBackground object when video is disabled. This prevents any video rendering + // code from running. Hence the Loader to completely remove it. + id: videoStreamLoader + anchors.fill: videoContentArea + visible: _showStreamLoader + sourceComponent: videoBackgroundComponent + + property bool videoDisabled: QGroundControl.settingsManager.videoSettings.videoSource.rawValue === QGroundControl.settingsManager.videoSettings.disabledVideoSource + } + //-- UVC Video (USB Camera or Video Device) + Loader { + id: cameraLoader + anchors.fill: videoContentArea + visible: _showUvcLoader + source: QGroundControl.videoManager.uvcEnabled ? "qrc:/qml/QGroundControl/FlyView/FlightDisplayViewUVC.qml" : "qrc:/qml/QGroundControl/FlyView//FlightDisplayViewDummy.qml" + } + + Item { + id: videoContentArea + height: parent.getHeight() + width: parent.getWidth() + anchors.centerIn: parent + visible: _showStreamLoader || _showUvcLoader + + // grid lines + Item { + anchors.fill: parent + visible: _showGrid && !QGroundControl.videoManager.fullScreen + Rectangle { color: Qt.rgba(1,1,1,0.5) height: parent.height width: 1 x: parent.width * 0.33 - visible: _showGrid && !QGroundControl.videoManager.fullScreen } Rectangle { color: Qt.rgba(1,1,1,0.5) height: parent.height width: 1 x: parent.width * 0.66 - visible: _showGrid && !QGroundControl.videoManager.fullScreen } Rectangle { color: Qt.rgba(1,1,1,0.5) width: parent.width height: 1 y: parent.height * 0.33 - visible: _showGrid && !QGroundControl.videoManager.fullScreen } Rectangle { color: Qt.rgba(1,1,1,0.5) width: parent.width height: 1 y: parent.height * 0.66 - visible: _showGrid && !QGroundControl.videoManager.fullScreen } } } - Loader { - // GStreamer is causing crashes on Lenovo laptop OpenGL Intel drivers. In order to workaround this - // we don't load a QGCVideoBackground object when video is disabled. This prevents any video rendering - // code from running. Hence the Loader to completely remove it. - height: parent.getHeight() - width: parent.getWidth() - anchors.centerIn: parent - visible: QGroundControl.videoManager.decoding - sourceComponent: videoBackgroundComponent - - property bool videoDisabled: QGroundControl.settingsManager.videoSettings.videoSource.rawValue === QGroundControl.settingsManager.videoSettings.disabledVideoSource - } //-- Thermal Image Item { diff --git a/src/FlyView/FlyViewVideo.qml b/src/FlyView/FlyViewVideo.qml index 78d9fcdb7499..f59d9cc92250 100644 --- a/src/FlyView/FlyViewVideo.qml +++ b/src/FlyView/FlyViewVideo.qml @@ -47,14 +47,7 @@ Item { id: videoStreaming anchors.fill: parent useSmallFont: _root.pipState.state !== _root.pipState.fullState - visible: QGroundControl.videoManager.isStreamSource - } - //-- UVC Video (USB Camera or Video Device) - Loader { - id: cameraLoader - anchors.fill: parent - visible: QGroundControl.videoManager.isUvc - source: QGroundControl.videoManager.uvcEnabled ? "qrc:/qml/QGroundControl/FlyView/FlightDisplayViewUVC.qml" : "qrc:/qml/QGroundControl/FlyView//FlightDisplayViewDummy.qml" + visible: QGroundControl.videoManager.isStreamSource || QGroundControl.videoManager.isUvc } QGCLabel { From 6e28575faeef7e34e50f657a8c9503d79ac7191f Mon Sep 17 00:00:00 2001 From: Diego Borrajo Date: Thu, 12 Mar 2026 07:45:47 +0100 Subject: [PATCH 075/119] Pre-compile QRegularExpression in BoardRegExpFallback_t (#14045) --- src/Comms/QGCSerialPortInfo.cc | 22 ++++++++++++++++++---- src/Comms/QGCSerialPortInfo.h | 3 ++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Comms/QGCSerialPortInfo.cc b/src/Comms/QGCSerialPortInfo.cc index e66c4442c815..ad1c338cf756 100644 --- a/src/Comms/QGCSerialPortInfo.cc +++ b/src/Comms/QGCSerialPortInfo.cc @@ -110,8 +110,15 @@ bool QGCSerialPortInfo::_loadJsonData() return false; } + const QRegularExpression regExp(fallbackObject[_jsonRegExpKey].toString(), QRegularExpression::CaseInsensitiveOption); + if (!regExp.isValid()) { + qCWarning(QGCSerialPortInfoLog) << "Invalid regular expression in board description fallback:" + << regExp.errorString() + << "pattern:" << fallbackObject[_jsonRegExpKey].toString(); + return false; + } const BoardRegExpFallback_t boardFallback = { - fallbackObject[_jsonRegExpKey].toString(), + regExp, _boardClassStringToType(fallbackObject[_jsonBoardClassKey].toString()), fallbackObject[_jsonAndroidOnlyKey].toBool(false) }; @@ -136,8 +143,15 @@ bool QGCSerialPortInfo::_loadJsonData() return false; } + const QRegularExpression regExp(fallbackObject[_jsonRegExpKey].toString(), QRegularExpression::CaseInsensitiveOption); + if (!regExp.isValid()) { + qCWarning(QGCSerialPortInfoLog) << "Invalid regular expression in board manufacturer fallback:" + << regExp.errorString() + << "pattern:" << fallbackObject[_jsonRegExpKey].toString(); + return false; + } const BoardRegExpFallback_t boardFallback = { - fallbackObject[_jsonRegExpKey].toString(), + regExp, _boardClassStringToType(fallbackObject[_jsonBoardClassKey].toString()), fallbackObject[_jsonAndroidOnlyKey].toBool(false) }; @@ -195,7 +209,7 @@ bool QGCSerialPortInfo::getBoardInfo(QGCSerialPortInfo::BoardType_t &boardType, Q_ASSERT(boardType == BoardTypeUnknown); for (const BoardRegExpFallback_t &boardFallback : _boardDescriptionFallbackList) { - if (description().contains(QRegularExpression(boardFallback.regExp, QRegularExpression::CaseInsensitiveOption))) { + if (description().contains(boardFallback.regExp)) { #ifndef Q_OS_ANDROID if (boardFallback.androidOnly) { continue; @@ -208,7 +222,7 @@ bool QGCSerialPortInfo::getBoardInfo(QGCSerialPortInfo::BoardType_t &boardType, } for (const BoardRegExpFallback_t &boardFallback : _boardManufacturerFallbackList) { - if (manufacturer().contains(QRegularExpression(boardFallback.regExp, QRegularExpression::CaseInsensitiveOption))) { + if (manufacturer().contains(boardFallback.regExp)) { #ifndef Q_OS_ANDROID if (boardFallback.androidOnly) { continue; diff --git a/src/Comms/QGCSerialPortInfo.h b/src/Comms/QGCSerialPortInfo.h index 79c70966f9f1..500ff992053d 100644 --- a/src/Comms/QGCSerialPortInfo.h +++ b/src/Comms/QGCSerialPortInfo.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #ifdef Q_OS_ANDROID #include "qserialportinfo.h" @@ -70,7 +71,7 @@ class QGCSerialPortInfo : public QSerialPortInfo static QList _boardInfoList; struct BoardRegExpFallback_t { - QString regExp; + QRegularExpression regExp; BoardType_t boardType; bool androidOnly; }; From c236f7fa5bba96691cc943d8d6f077e98d2625f1 Mon Sep 17 00:00:00 2001 From: Diego Borrajo Date: Thu, 12 Mar 2026 07:50:48 +0100 Subject: [PATCH 076/119] Compass: fix label positioning, readability in lock nose-up mode, and code cleanup (#14051) --- src/FlightMap/Widgets/CompassDial.qml | 4 +++ src/FlightMap/Widgets/QGCCompassWidget.qml | 29 ++++++---------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/FlightMap/Widgets/CompassDial.qml b/src/FlightMap/Widgets/CompassDial.qml index 253b316eca3f..0e9d23f3f4f6 100644 --- a/src/FlightMap/Widgets/CompassDial.qml +++ b/src/FlightMap/Widgets/CompassDial.qml @@ -21,6 +21,7 @@ Item { QGCLabel { anchors.centerIn: parent text: "N" + rotation: _lockNoseUpCompass ? _heading : 0 transform: Translate { x: translateCenterToAngleX(control.offsetRadius, 0) @@ -31,6 +32,7 @@ Item { QGCLabel { anchors.centerIn: parent text: "E" + rotation: _lockNoseUpCompass ? _heading : 0 transform: Translate { x: translateCenterToAngleX(control.offsetRadius, 90) @@ -41,6 +43,7 @@ Item { QGCLabel { anchors.centerIn: parent text: "S" + rotation: _lockNoseUpCompass ? _heading : 0 transform: Translate { x: translateCenterToAngleX(control.offsetRadius, 180) @@ -51,6 +54,7 @@ Item { QGCLabel { anchors.centerIn: parent text: "W" + rotation: _lockNoseUpCompass ? _heading : 0 transform: Translate { x: translateCenterToAngleX(control.offsetRadius, 270) diff --git a/src/FlightMap/Widgets/QGCCompassWidget.qml b/src/FlightMap/Widgets/QGCCompassWidget.qml index 4f7616ce00c8..cd8890b71cc9 100644 --- a/src/FlightMap/Widgets/QGCCompassWidget.qml +++ b/src/FlightMap/Widgets/QGCCompassWidget.qml @@ -58,12 +58,7 @@ Rectangle { Item { id: rotationParent anchors.fill: parent - - transform: Rotation { - origin.x: rotationParent.width / 2 - origin.y: rotationParent.height / 2 - angle: _lockNoseUpCompass ? -_heading : 0 - } + rotation: _lockNoseUpCompass ? -_heading : 0 CompassDial { anchors.fill: parent @@ -84,12 +79,7 @@ Rectangle { anchors.fill: parent sourceSize.height: parent.height visible: showCOG() - - transform: Rotation { - origin.x: cogPointer.width / 2 - origin.y: cogPointer.height / 2 - angle: _courseOverGround - } + rotation: _courseOverGround } Image { @@ -100,12 +90,7 @@ Rectangle { anchors.fill: parent sourceSize.height: parent.height visible: showHeadingToNextWP() - - transform: Rotation { - origin.x: nextWPPointer.width / 2 - origin.y: nextWPPointer.height / 2 - angle: _headingToNextWP - } + rotation: _headingToNextWP } // Launch location indicator @@ -123,13 +108,15 @@ Rectangle { font.bold: true color: qgcPal.text anchors.centerIn: parent + rotation: _lockNoseUpCompass ? _heading : 0 } transform: Translate { - property double _angle: _headingToHome + property double _angle: _headingToHome + property real _labelOffset: root.width / 2 + ScreenTools.defaultFontPixelHeight / 2 - x: translateCenterToAngleX(parent.width / 2, _angle) - y: translateCenterToAngleY(parent.height / 2, _angle) + x: translateCenterToAngleX(_labelOffset, _angle) + y: translateCenterToAngleY(_labelOffset, _angle) } } } From e5cce9a17d57581ffbc8bc6c2cc155043b7f5f0a Mon Sep 17 00:00:00 2001 From: John Nomikos <84941452+jnomikos@users.noreply.github.com> Date: Thu, 12 Mar 2026 04:13:30 -0500 Subject: [PATCH 077/119] Remote ID: Fix zero and negative GCS GPS altitude being invalid (#13378) Co-authored-by: Holden Ramsey --- src/PositionManager/PositionManager.cpp | 14 ++-- src/PositionManager/PositionManager.h | 7 +- src/Vehicle/RemoteIDManager.cc | 90 ++++++++++++------------- src/Vehicle/RemoteIDManager.h | 4 ++ 4 files changed, 61 insertions(+), 54 deletions(-) diff --git a/src/PositionManager/PositionManager.cpp b/src/PositionManager/PositionManager.cpp index e4da61421da0..1e4670419f52 100644 --- a/src/PositionManager/PositionManager.cpp +++ b/src/PositionManager/PositionManager.cpp @@ -7,8 +7,6 @@ #include #include -#include -#include #include QGC_LOGGING_CATEGORY(QGCPositionManagerLog, "PositionManager.QGCPositionManager") @@ -106,6 +104,7 @@ void QGCPositionManager::setNmeaSourceDevice(QIODevice *device) void QGCPositionManager::_positionUpdated(const QGeoPositionInfo &update) { _geoPositionInfo = update; + _gcsPositioningError = QGeoPositionInfoSource::NoError; QGeoCoordinate newGCSPosition(_gcsPosition); @@ -143,6 +142,12 @@ void QGCPositionManager::_positionUpdated(const QGeoPositionInfo &update) emit positionInfoUpdated(update); } +void QGCPositionManager::_positionError(QGeoPositionInfoSource::Error gcsPositioningError) +{ + qCWarning(QGCPositionManagerLog) << Q_FUNC_INFO << "Positioning error:" << gcsPositioningError; + _gcsPositioningError = gcsPositioningError; +} + void QGCPositionManager::_setGCSHeading(qreal newGCSHeading) { if (newGCSHeading != _gcsHeading) { @@ -201,10 +206,9 @@ void QGCPositionManager::_setPositionSource(QGCPositionSource source) #if !defined(Q_OS_DARWIN) && !defined(Q_OS_IOS) _currentSource->setUpdateInterval(_updateInterval); #endif + (void) connect(_currentSource, &QGeoPositionInfoSource::positionUpdated, this, &QGCPositionManager::_positionUpdated); - (void) connect(_currentSource, &QGeoPositionInfoSource::errorOccurred, this, [](QGeoPositionInfoSource::Error positioningError) { - qCWarning(QGCPositionManagerLog) << Q_FUNC_INFO << positioningError; - }); + (void) connect(_currentSource, &QGeoPositionInfoSource::errorOccurred, this, &QGCPositionManager::_positionError); // (void) connect(QGCCompass::instance(), &QGCCompass::positionUpdated, this, &QGCPositionManager::_positionUpdated); diff --git a/src/PositionManager/PositionManager.h b/src/PositionManager/PositionManager.h index 516628f66c84..40490ed8ae25 100644 --- a/src/PositionManager/PositionManager.h +++ b/src/PositionManager/PositionManager.h @@ -4,11 +4,11 @@ #include #include #include +#include #include Q_DECLARE_LOGGING_CATEGORY(QGCPositionManagerLog) -class QGeoPositionInfoSource; class QNmeaPositionInfoSource; class QGCCompass; @@ -35,6 +35,8 @@ class QGCPositionManager : public QObject qreal gcsHeading() const { return _gcsHeading; } qreal gcsPositionHorizontalAccuracy() const { return _gcsPositionHorizontalAccuracy; } QGeoPositionInfo geoPositionInfo() const { return _geoPositionInfo; } + QGeoPositionInfoSource::Error gcsPositioningError() const { return _gcsPositioningError; } + int updateInterval() const { return _updateInterval; } void setNmeaSourceDevice(QIODevice *device); @@ -47,6 +49,7 @@ class QGCPositionManager : public QObject private slots: void _positionUpdated(const QGeoPositionInfo &update); + void _positionError(QGeoPositionInfoSource::Error gcsPositioningError); private: enum QGCPositionSource { @@ -68,6 +71,8 @@ private slots: int _updateInterval = 0; QGeoPositionInfo _geoPositionInfo; + QGeoPositionInfoSource::Error _gcsPositioningError = QGeoPositionInfoSource::NoError; + QGeoCoordinate _gcsPosition; qreal _gcsHeading = qQNaN(); qreal _gcsPositionHorizontalAccuracy = std::numeric_limits::infinity(); diff --git a/src/Vehicle/RemoteIDManager.cc b/src/Vehicle/RemoteIDManager.cc index d9502d96819f..47bd23c671c4 100644 --- a/src/Vehicle/RemoteIDManager.cc +++ b/src/Vehicle/RemoteIDManager.cc @@ -256,66 +256,60 @@ void RemoteIDManager::_sendOperatorID() } } +void RemoteIDManager::_updateGcsGpsStatus(bool gpsGood, const QString& error) +{ + if (!error.isEmpty() && _gcsGPSError != error) { + _gcsGPSError = error; + qCWarning(RemoteIDManagerLog) << "GCS GPS error:" << error; + } + if (_gcsGPSGood != gpsGood) { + _gcsGPSGood = gpsGood; + if (gpsGood) { + _gcsGPSError.clear(); + } + emit gcsGPSGoodChanged(); + } +} + void RemoteIDManager::_sendSystem() { - QGeoCoordinate gcsPosition; - QGeoPositionInfo geoPositionInfo; + QGeoCoordinate gcsPosition(0, 0, 0); + const uint32_t locationType = _settings->locationType()->rawValue().toUInt(); // Location types: // 0 -> TAKEOFF (not supported yet) // 1 -> LIVE GNNS // 2 -> FIXED - if (_settings->locationType()->rawValue().toUInt() == LocationTypes::FIXED) { + if (locationType == LocationTypes::FIXED) { + const double lat = _settings->latitudeFixed()->rawValue().toDouble(); + const double lon = _settings->longitudeFixed()->rawValue().toDouble(); + const double alt = _settings->altitudeFixed()->rawValue().toDouble(); + // For FIXED location, we first check that the values are valid. Then we populate our position - if (_settings->latitudeFixed()->rawValue().toFloat() >= -90 && _settings->latitudeFixed()->rawValue().toFloat() <= 90 && _settings->longitudeFixed()->rawValue().toFloat() >= -180 && _settings->longitudeFixed()->rawValue().toFloat() <= 180) { - gcsPosition = QGeoCoordinate(_settings->latitudeFixed()->rawValue().toFloat(), _settings->longitudeFixed()->rawValue().toFloat(), _settings->altitudeFixed()->rawValue().toFloat()); - geoPositionInfo = QGeoPositionInfo(gcsPosition, QDateTime::currentDateTime().currentDateTimeUtc()); - if (!_gcsGPSGood) { - _gcsGPSGood = true; - emit gcsGPSGoodChanged(); - } + if (lat >= -90.0 && lat <= 90.0 && lon >= -180.0 && lon <= 180.0) { + gcsPosition = QGeoCoordinate(lat, lon, alt); + _updateGcsGpsStatus(true); } else { - gcsPosition = QGeoCoordinate(0,0,0); - geoPositionInfo = QGeoPositionInfo(gcsPosition, QDateTime::currentDateTime().currentDateTimeUtc()); - if (_gcsGPSGood) { - _gcsGPSGood = false; - emit gcsGPSGoodChanged(); - qCDebug(RemoteIDManagerLog) << "The provided coordinates for FIXED position are invalid."; - } + _updateGcsGpsStatus(false, "The provided coordinates for FIXED position are invalid."); } } else { - // For Live GNSS we take QGC GPS data - gcsPosition = QGCPositionManager::instance()->gcsPosition(); - geoPositionInfo = QGCPositionManager::instance()->geoPositionInfo(); - - // GPS position needs to be valid before checking other stuff - if (geoPositionInfo.isValid()) { - // If we dont have altitude for FAA then the GPS data is no good - if ((_settings->region()->rawValue().toInt() == Region::FAA) && !(gcsPosition.altitude() >= 0) && _gcsGPSGood) { - _gcsGPSGood = false; - emit gcsGPSGoodChanged(); - qCDebug(RemoteIDManagerLog) << "GCS GPS data error (no altitude): Altitude data is mandatory for GCS GPS data in FAA regions."; - return; - } - - // If the GPS data is older than ALLOWED_GPS_DELAY we cannot use this data - if (_lastGeoPositionTimeStamp.msecsTo(QDateTime::currentDateTime().currentDateTimeUtc()) > ALLOWED_GPS_DELAY) { - if (_gcsGPSGood) { - _gcsGPSGood = false; - emit gcsGPSGoodChanged(); - qCDebug(RemoteIDManagerLog) << "GCS GPS data is older than 5 seconds"; - } - } else { - if (!_gcsGPSGood) { - _gcsGPSGood = true; - emit gcsGPSGoodChanged(); - } - } + QGCPositionManager* positionManager = QGCPositionManager::instance(); + QGeoPositionInfo geoPositionInfo = positionManager->geoPositionInfo(); + gcsPosition = positionManager->gcsPosition(); + + if (!geoPositionInfo.isValid()) { + _updateGcsGpsStatus(false, "GCS GPS data is not valid."); + } else if (positionManager->gcsPositioningError() != QGeoPositionInfoSource::NoError && positionManager->gcsPositioningError() != QGeoPositionInfoSource::UpdateTimeoutError) { + _updateGcsGpsStatus(false, QString("GCS GPS data error: %1").arg(positionManager->gcsPositioningError())); + } else if (!gcsPosition.isValid() || gcsPosition.type() == QGeoCoordinate::InvalidCoordinate) { + _updateGcsGpsStatus(false, "GCS GPS data error: Invalid coordinate type."); + } else if (_settings->region()->rawValue().toInt() == Region::FAA && gcsPosition.type() != QGeoCoordinate::Coordinate3D) { + // FAA requires altitude data, or else the GPS data is not good + _updateGcsGpsStatus(false, "GCS GPS data error: Altitude data is mandatory for FAA regions."); + } else if (_lastGeoPositionTimeStamp.msecsTo(QDateTime::currentDateTime().currentDateTimeUtc()) > ALLOWED_GPS_DELAY) { + _updateGcsGpsStatus(false, "GCS GPS data is older than 5 seconds"); } else { - _gcsGPSGood = false; - emit gcsGPSGoodChanged(); - qCDebug(RemoteIDManagerLog) << "GCS GPS data is not valid."; + _updateGcsGpsStatus(true); } - } WeakLinkInterfacePtr weakLink = _vehicle->vehicleLinkManager()->primaryLink(); diff --git a/src/Vehicle/RemoteIDManager.h b/src/Vehicle/RemoteIDManager.h index 5b4ae7bcdad4..3327391f62f2 100644 --- a/src/Vehicle/RemoteIDManager.h +++ b/src/Vehicle/RemoteIDManager.h @@ -94,6 +94,9 @@ private slots: // Basic ID void _sendBasicID(); + // GCS GPS status + void _updateGcsGpsStatus(bool gpsGood, const QString& error = QString()); + bool _isEUOperatorIDValid(const QString& operatorID) const; QChar _calculateLuhnMod36(const QString& input) const; @@ -106,6 +109,7 @@ private slots: QString _armStatusError; bool _commsGood; bool _gcsGPSGood; + QString _gcsGPSError; bool _basicIDGood; bool _GCSBasicIDValid; bool _operatorIDGood; From b0a357ccb0a06d1831f2e12370d9f57c6b677c44 Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Thu, 12 Mar 2026 02:24:24 -0400 Subject: [PATCH 078/119] Async GStreamer init, CI tooling cleanup, and GstVideoReceiver fixes - Make GStreamer initialization async to avoid blocking the main thread - Add GStreamer unit tests with environment isolation - Fix resource leaks in GstVideoReceiver error paths (decoder branch cleanup) - Consolidate CI GStreamer action and build script improvements - Remove resolve_gstreamer_config.py and setup_bootstrap.py in favor of read_config.py - Bump default GStreamer version to 1.24.13 --- .github/actions/gstreamer/action.yml | 61 +++-- .github/workflows/build-gstreamer.yml | 1 - src/QGCApplication.cc | 11 +- src/QGCApplication.h | 4 +- src/VideoManager/VideoManager.cc | 203 +++++++++++--- src/VideoManager/VideoManager.h | 40 +-- .../VideoReceiver/GStreamer/GStreamer.cc | 164 +++++++----- .../VideoReceiver/GStreamer/GStreamer.h | 14 +- .../GStreamer/GStreamerHelpers.cc | 51 +--- .../GStreamer/GStreamerHelpers.h | 3 +- .../GStreamer/GStreamerLogging.cc | 26 +- .../GStreamer/GStreamerLogging.h | 8 - .../GStreamer/GstVideoReceiver.cc | 38 +-- .../GStreamer/GstVideoReceiver.h | 1 - .../VideoReceiver/GStreamer/README.md | 4 +- src/main.cc | 4 + test/CMakeLists.txt | 6 + test/VideoManager/CMakeLists.txt | 5 + test/VideoManager/GStreamer/CMakeLists.txt | 12 + test/VideoManager/GStreamer/GStreamerTest.cc | 252 ++++++++++++++++++ test/VideoManager/GStreamer/GStreamerTest.h | 20 ++ tools/setup/build-gstreamer.py | 50 ++-- tools/setup/download_artifacts.py | 108 +++++--- tools/setup/install_dependencies.py | 49 ++-- tools/setup/install_python.py | 12 +- tools/setup/read_config.py | 114 ++++++-- tools/setup/resolve_gstreamer_config.py | 61 ----- tools/setup/setup_bootstrap.py | 14 - tools/tests/test_download_artifacts.py | 24 +- tools/tests/test_resolve_gstreamer_config.py | 19 -- 30 files changed, 888 insertions(+), 491 deletions(-) create mode 100644 test/VideoManager/CMakeLists.txt create mode 100644 test/VideoManager/GStreamer/CMakeLists.txt create mode 100644 test/VideoManager/GStreamer/GStreamerTest.cc create mode 100644 test/VideoManager/GStreamer/GStreamerTest.h delete mode 100644 tools/setup/resolve_gstreamer_config.py delete mode 100644 tools/setup/setup_bootstrap.py delete mode 100644 tools/tests/test_resolve_gstreamer_config.py diff --git a/.github/actions/gstreamer/action.yml b/.github/actions/gstreamer/action.yml index 46224ba4c2e1..7e4e95b9553f 100644 --- a/.github/actions/gstreamer/action.yml +++ b/.github/actions/gstreamer/action.yml @@ -24,14 +24,11 @@ inputs: description: 'Upload to S3' required: false default: 'true' - aws-role-arn: - description: 'AWS IAM role ARN for OIDC authentication (preferred over static credentials)' - required: false aws-access-key-id: - description: 'AWS access key ID (fallback if OIDC not available)' + description: 'AWS access key ID' required: false aws-secret-access-key: - description: 'AWS secret access key (fallback if OIDC not available)' + description: 'AWS secret access key' required: false outputs: @@ -45,7 +42,36 @@ runs: - name: Read build config id: config shell: bash - run: python3 ./tools/setup/resolve_gstreamer_config.py --platform "${{ inputs.platform }}" --version "${{ inputs.version }}" + env: + INPUT_VERSION: ${{ inputs.version }} + INPUT_PLATFORM: ${{ inputs.platform }} + INPUT_ARCH: ${{ inputs.arch }} + run: | + set -euo pipefail + if command -v python3 >/dev/null 2>&1; then + py_cmd="python3" + elif command -v python >/dev/null 2>&1; then + py_cmd="python" + else + echo "Error: Python interpreter not found in PATH" >&2 + exit 1 + fi + echo "py_cmd=$py_cmd" >> "$GITHUB_OUTPUT" + + version="${INPUT_VERSION}" + if [[ -z "$version" ]]; then + case "${INPUT_PLATFORM}" in + macos) version_key="gstreamer_macos_version" ;; + windows) version_key="gstreamer_windows_version" ;; + android) version_key="gstreamer_android_version" ;; + ios) version_key="gstreamer_ios_version" ;; + *) version_key="gstreamer_default_version" ;; + esac + version=$("${py_cmd}" ./tools/setup/read_config.py --get "${version_key}") + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "qt_version=$("${py_cmd}" ./tools/setup/read_config.py --get qt_version)" >> "$GITHUB_OUTPUT" + echo "Building GStreamer $version for ${INPUT_PLATFORM} (${INPUT_ARCH})" # ===== DEPENDENCIES ===== @@ -258,27 +284,8 @@ runs: $libCount = (Get-ChildItem (Join-Path $prefix 'lib') -Filter '*.lib' | Measure-Object).Count Write-Host "$libCount library files found" - $env:PATH = (Join-Path $prefix 'bin') + ";" + $env:PATH - $env:GST_PLUGIN_PATH = Join-Path $prefix 'lib' 'gstreamer-1.0' - & (Join-Path $prefix 'bin' 'gst-launch-1.0.exe') --version - # ===== ARCHIVE & UPLOAD ===== - - name: Configure AWS Credentials (OIDC) - if: inputs.upload-s3 == 'true' && inputs.aws-role-arn != '' - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ inputs.aws-role-arn }} - aws-region: us-west-2 - - - name: Configure AWS Credentials (Static) - if: inputs.upload-s3 == 'true' && inputs.aws-role-arn == '' && inputs.aws-access-key-id != '' - uses: aws-actions/configure-aws-credentials@v6 - with: - aws-access-key-id: ${{ inputs.aws-access-key-id }} - aws-secret-access-key: ${{ inputs.aws-secret-access-key }} - aws-region: us-west-2 - - name: Create archive id: archive shell: bash @@ -291,6 +298,8 @@ runs: SIMULATOR: ${{ inputs.simulator }} UPLOAD_S3: ${{ inputs.upload-s3 }} WORKSPACE: ${{ github.workspace }} + AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }} + AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }} AWS_DEFAULT_REGION: us-west-2 run: | set -euo pipefail @@ -301,7 +310,7 @@ runs: --output-dir "$OUTPUT_DIR" ) [[ "$SIMULATOR" == "true" ]] && args+=(--simulator) - [[ "$UPLOAD_S3" == "true" ]] && args+=(--upload-s3) + [[ "$UPLOAD_S3" == "true" && -n "$AWS_ACCESS_KEY_ID" ]] && args+=(--upload-s3) "${PY_CMD}" "${WORKSPACE}/.github/scripts/gstreamer_archive.py" "${args[@]}" diff --git a/.github/workflows/build-gstreamer.yml b/.github/workflows/build-gstreamer.yml index 3baa3e519d17..d1435da49971 100644 --- a/.github/workflows/build-gstreamer.yml +++ b/.github/workflows/build-gstreamer.yml @@ -96,6 +96,5 @@ jobs: build-type: ${{ inputs.build_type }} simulator: ${{ inputs.simulator }} upload-s3: ${{ inputs.upload_s3 }} - aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/src/QGCApplication.cc b/src/QGCApplication.cc index bb6381a3e33a..b504457a2abe 100644 --- a/src/QGCApplication.cc +++ b/src/QGCApplication.cc @@ -228,13 +228,13 @@ void QGCApplication::init() if (_simpleBootTest) { // Since GStream builds are so problematic we initialize video during the simple boot test // to make sure it works and verfies plugin availability. - _initVideo(); + _bootTestPassed = _initVideo(); } else if (!_runningUnitTests) { _initForNormalAppBoot(); } } -void QGCApplication::_initVideo() +bool QGCApplication::_initVideo() { #ifdef QGC_GST_STREAMING // Gstreamer video playback requires OpenGL @@ -242,13 +242,16 @@ void QGCApplication::_initVideo() #endif QGCCorePlugin::instance(); // CorePlugin must be initialized before VideoManager for Video Cleanup - VideoManager::instance(); + VideoManager *videoManager = VideoManager::instance(); + videoManager->startGStreamerInit(); + const bool initSucceeded = !_simpleBootTest || videoManager->waitForGStreamerInit(); _videoManagerInitialized = true; + return initSucceeded; } void QGCApplication::_initForNormalAppBoot() { - _initVideo(); // GStreamer must be initialized before QmlEngine + (void) _initVideo(); QQuickStyle::setStyle("Basic"); QGCCorePlugin::instance()->init(); diff --git a/src/QGCApplication.h b/src/QGCApplication.h index 3d80a9fb9a3e..dc5a59973b33 100644 --- a/src/QGCApplication.h +++ b/src/QGCApplication.h @@ -56,6 +56,7 @@ class QGCApplication : public QApplication bool runningUnitTests() const { return _runningUnitTests; } bool simpleBootTest() const { return _simpleBootTest; } + bool bootTestPassed() const { return _bootTestPassed; } /// Returns true if Qt debug output should be logged to a file bool logOutput() const { return _logOutput; } @@ -125,7 +126,7 @@ private slots: private: bool compressEvent(QEvent *event, QObject *receiver, QPostEventList *postedEvents) final; - void _initVideo(); + bool _initVideo(); /// Initialize the application for normal application boot. Or in other words we are not going to run unit tests. void _initForNormalAppBoot(); @@ -156,6 +157,7 @@ private slots: bool _showErrorsInToolbar = false; QElapsedTimer _msecsElapsedTime; bool _videoManagerInitialized = false; + bool _bootTestPassed = true; QList> _delayedAppMessages; diff --git a/src/VideoManager/VideoManager.cc b/src/VideoManager/VideoManager.cc index 553772074ab8..c96e4ff190a6 100644 --- a/src/VideoManager/VideoManager.cc +++ b/src/VideoManager/VideoManager.cc @@ -11,21 +11,23 @@ #include "Vehicle.h" #include "VideoReceiver.h" #include "VideoSettings.h" -#ifdef QGC_GST_STREAMING -#include "GStreamer.h" -#include "VideoItemStub.h" -#else #include "VideoItemStub.h" -#endif #include "QtMultimediaReceiver.h" #include "UVCReceiver.h" +#ifdef QGC_GST_STREAMING +#include "GStreamer.h" +#include "GStreamerHelpers.h" +#endif #include #include +#include +#include +#include +#include #include #include #include -#include QGC_LOGGING_CATEGORY(VideoManagerLog, "Video.VideoManager") @@ -37,6 +39,11 @@ static constexpr const char *kFileExtension[VideoReceiver::FILE_FORMAT_MAX + 1] Q_APPLICATION_STATIC(VideoManager, _videoManagerInstance); +bool VideoManager::_shouldSkipGStreamerForUnitTests() +{ + return qgcApp() && qgcApp()->runningUnitTests() && !qEnvironmentVariableIsSet("QGC_TEST_ENABLE_GSTREAMER"); +} + VideoManager::VideoManager(QObject *parent) : QObject(parent) , _subtitleWriter(new SubtitleWriter(this)) @@ -46,19 +53,17 @@ VideoManager::VideoManager(QObject *parent) (void) qRegisterMetaType("STATUS"); + bool needsStub = true; #ifdef QGC_GST_STREAMING - const bool skipGStreamerForUnitTests = - qgcApp() && qgcApp()->runningUnitTests() && !qEnvironmentVariableIsSet("QGC_TEST_ENABLE_GSTREAMER"); - - if (skipGStreamerForUnitTests) { - (void) qmlRegisterType("org.freedesktop.gstreamer.Qt6GLVideoItem", 1, 0, "GstGLQt6VideoItem"); + _gstreamerDisabledForUnitTests = _shouldSkipGStreamerForUnitTests(); + needsStub = _gstreamerDisabledForUnitTests; + if (_gstreamerDisabledForUnitTests) { qCInfo(VideoManagerLog) << "Skipping GStreamer initialization for unit tests"; - } else if (!GStreamer::initialize()) { - qCCritical(VideoManagerLog) << "Failed To Initialize GStreamer"; } -#else - (void) qmlRegisterType("org.freedesktop.gstreamer.Qt6GLVideoItem", 1, 0, "GstGLQt6VideoItem"); #endif + if (needsStub) { + (void) qmlRegisterType("org.freedesktop.gstreamer.Qt6GLVideoItem", 1, 0, "GstGLQt6VideoItem"); + } } VideoManager::~VideoManager() @@ -71,6 +76,85 @@ VideoManager *VideoManager::instance() return _videoManagerInstance(); } +void VideoManager::startGStreamerInit() +{ +#ifdef QGC_GST_STREAMING + if (_gstreamerDisabledForUnitTests) { + _initState = InitState::GstReady; + qCInfo(VideoManagerLog) << "GStreamer initialization disabled for unit tests"; + return; + } + + if (_initState != InitState::NotStarted) { + qCWarning(VideoManagerLog) << "GStreamer init already started"; + return; + } + + _initState = InitState::Pending; + _gstInitFuture = GStreamer::initializeAsync(); + _gstInitFuture.then(this, [this](bool success) { + _onGstInitComplete(success); + }).onCanceled(this, [this] { + _onGstInitComplete(false); + }); +#endif +} + +bool VideoManager::waitForGStreamerInit(int timeoutMs) +{ +#ifdef QGC_GST_STREAMING + if (_gstreamerDisabledForUnitTests) { + return true; + } + + if (_initState == InitState::NotStarted) { + startGStreamerInit(); + } + + switch (_initState) { + case InitState::Failed: + return false; + case InitState::GstReady: + case InitState::Running: + return true; + default: + break; + } + + if (!_gstInitFuture.isValid()) { + qCCritical(VideoManagerLog) << "waitForGStreamerInit: no valid future"; + return false; + } + + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + QFutureWatcher watcher; + (void) connect(&watcher, &QFutureWatcher::finished, &loop, &QEventLoop::quit); + (void) connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + + watcher.setFuture(_gstInitFuture); + if (!watcher.isFinished()) { + timer.start(timeoutMs); + loop.exec(); + } + + if (!watcher.isFinished()) { + qCCritical(VideoManagerLog) << "Timed out waiting for GStreamer init"; + return false; + } + + const bool success = watcher.result(); + if (_initState == InitState::Pending || _initState == InitState::QmlReady) { + _onGstInitComplete(success); + } + return _initState != InitState::Failed; +#else + Q_UNUSED(timeoutMs); + return true; +#endif +} + void VideoManager::init(QQuickWindow *mainWindow) { if (_initialized) { @@ -95,25 +179,83 @@ void VideoManager::init(QQuickWindow *mainWindow) (void) connect(this, &VideoManager::autoStreamConfiguredChanged, this, &VideoManager::_videoSourceChanged); - _mainWindow->scheduleRenderJob(new FinishVideoInitialization(), QQuickWindow::AfterSynchronizingStage); +#ifdef QGC_GST_STREAMING + if (_initState == InitState::NotStarted) { + startGStreamerInit(); + } +#endif + + _mainWindow->scheduleRenderJob( + QRunnable::create([this] { + QMetaObject::invokeMethod(this, &VideoManager::_initAfterQmlIsReady, Qt::QueuedConnection); + }), + QQuickWindow::AfterSynchronizingStage); _initialized = true; } void VideoManager::_initAfterQmlIsReady() { - if (_initAfterQmlIsReadyDone) { - qCWarning(VideoManagerLog) << "_initAfterQmlIsReady called multiple times"; - return; - } if (!_mainWindow) { qCCritical(VideoManagerLog) << "_initAfterQmlIsReady called with NULL mainWindow"; return; } - _initAfterQmlIsReadyDone = true; qCDebug(VideoManagerLog) << "_initAfterQmlIsReady"; +#ifdef QGC_GST_STREAMING + switch (_initState) { + case InitState::Pending: + _initState = InitState::QmlReady; + qCDebug(VideoManagerLog) << "QML ready, waiting for GStreamer"; + return; + case InitState::GstReady: + _initState = InitState::Running; + qCDebug(VideoManagerLog) << "QML ready, GStreamer already done — creating receivers"; + break; + case InitState::Failed: + qCWarning(VideoManagerLog) << "QML ready but GStreamer init failed"; + return; + default: + qCWarning(VideoManagerLog) << "_initAfterQmlIsReady: unexpected state" << static_cast(_initState); + return; + } +#endif + _createVideoReceivers(); +} + +void VideoManager::_onGstInitComplete(bool success) +{ + if (!success) { + _initState = InitState::Failed; + qCCritical(VideoManagerLog) << "GStreamer initialization failed"; + return; + } + +#ifdef QGC_GST_STREAMING + const auto decoderOption = static_cast( + _videoSettings->forceVideoDecoder()->rawValue().toInt()); + GStreamer::setCodecPriorities(decoderOption); +#endif + + switch (_initState) { + case InitState::Pending: + _initState = InitState::GstReady; + qCDebug(VideoManagerLog) << "GStreamer ready, waiting for QML"; + return; + case InitState::QmlReady: + _initState = InitState::Running; + qCDebug(VideoManagerLog) << "GStreamer ready, QML already done — creating receivers"; + _createVideoReceivers(); + return; + default: + qCWarning(VideoManagerLog) << "_onGstInitComplete: unexpected state" << static_cast(_initState); + return; + } +} + +void VideoManager::_createVideoReceivers() +{ static const QStringList videoStreamList = { "videoContent", "thermalVideo" @@ -808,22 +950,3 @@ void VideoManager::startVideo() _restartAllVideos(); } - -/*===========================================================================*/ - -FinishVideoInitialization::FinishVideoInitialization() - : QRunnable() -{ - // qCDebug(VideoManagerLog) << this; -} - -FinishVideoInitialization::~FinishVideoInitialization() -{ - // qCDebug(VideoManagerLog) << this; -} - -void FinishVideoInitialization::run() -{ - qCDebug(VideoManagerLog) << "FinishVideoInitialization::run"; - QMetaObject::invokeMethod(VideoManager::instance(), &VideoManager::_initAfterQmlIsReady, Qt::QueuedConnection); -} diff --git a/src/VideoManager/VideoManager.h b/src/VideoManager/VideoManager.h index 04bca3349be4..dc7a4d0466b0 100644 --- a/src/VideoManager/VideoManager.h +++ b/src/VideoManager/VideoManager.h @@ -1,15 +1,14 @@ #pragma once +#include #include #include -#include #include #include Q_DECLARE_LOGGING_CATEGORY(VideoManagerLog) class QQuickWindow; -class FinishVideoInitialization; class SubtitleWriter; class Vehicle; class VideoReceiver; @@ -46,8 +45,6 @@ class VideoManager : public QObject explicit VideoManager(QObject *parent = nullptr); ~VideoManager(); - friend class FinishVideoInitialization; - static VideoManager *instance(); Q_INVOKABLE void grabImage(const QString &imageFile = QString()); @@ -57,6 +54,8 @@ class VideoManager : public QObject Q_INVOKABLE void stopVideo(); void init(QQuickWindow *mainWindow); + void startGStreamerInit(); + bool waitForGStreamerInit(int timeoutMs = 60000); void cleanup(); bool autoStreamConfigured() const; bool decoding() const { return _decoding; } @@ -101,7 +100,19 @@ private slots: void _videoSourceChanged(); private: + enum class InitState : uint8_t { + NotStarted, + Pending, + GstReady, + QmlReady, + Running, + Failed + }; + + static bool _shouldSkipGStreamerForUnitTests(); void _initAfterQmlIsReady(); + void _onGstInitComplete(bool success); + void _createVideoReceivers(); void _initVideoReceiver(VideoReceiver *receiver, QQuickWindow *window); bool _updateAutoStream(VideoReceiver *receiver); bool _updateUVC(VideoReceiver *receiver); @@ -114,30 +125,21 @@ private slots: static void _cleanupOldVideos(); QList _videoReceivers; - SubtitleWriter *_subtitleWriter = nullptr; VideoSettings *_videoSettings = nullptr; + QQuickWindow *_mainWindow = nullptr; + Vehicle *_activeVehicle = nullptr; + InitState _initState = InitState::NotStarted; + QFuture _gstInitFuture; bool _initialized = false; - bool _initAfterQmlIsReadyDone = false; + bool _gstreamerDisabledForUnitTests = false; bool _fullScreen = false; + QAtomicInteger _decoding = false; QAtomicInteger _recording = false; QAtomicInteger _streaming = false; QSize _videoSize; QString _imageFile; QString _uvcVideoSourceID; - Vehicle *_activeVehicle = nullptr; - QQuickWindow *_mainWindow = nullptr; -}; - -/*===========================================================================*/ - -class FinishVideoInitialization : public QRunnable -{ -public: - FinishVideoInitialization(); - ~FinishVideoInitialization(); - - void run() final; }; diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc index c5a3db933342..3f0794ce3974 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc @@ -1,15 +1,15 @@ #include "GStreamer.h" #include "GStreamerHelpers.h" #include "GStreamerLogging.h" +#include "AppSettings.h" #include "GstVideoReceiver.h" -#include "SettingsManager.h" -#include "VideoSettings.h" +#include #include #include #include -#include #include +#include #include #include @@ -235,7 +235,6 @@ void _clearManagedGstEnvVars() "GST_PLUGIN_SYSTEM_PATH", "GST_PLUGIN_PATH_1_0", "GST_PLUGIN_PATH", - "PYTHONNOUSERSITE", }; for (const char *name : varsToUnset) { @@ -252,54 +251,23 @@ void _setGstEnvIfExecutable(const char *name, const QString &path) } } -static constexpr const char *kPythonEnvVars[] = { - "PYTHONHOME", - "PYTHONPATH", - "VIRTUAL_ENV", - "CONDA_PREFIX", - "CONDA_DEFAULT_ENV", - "PYTHONUSERBASE", - "PYTHONNOUSERSITE", -}; - -QHash s_savedPythonEnv; -bool s_pythonEnvSanitized = false; - void _sanitizePythonEnvForScanner() { - s_savedPythonEnv.clear(); - s_pythonEnvSanitized = true; + static constexpr const char *varsToUnset[] = { + "PYTHONHOME", + "PYTHONPATH", + "VIRTUAL_ENV", + "CONDA_PREFIX", + "CONDA_DEFAULT_ENV", + "PYTHONUSERBASE", + }; - for (const char *name : kPythonEnvVars) { + for (const char *name : varsToUnset) { if (qEnvironmentVariableIsSet(name)) { - s_savedPythonEnv.insert(QByteArray(name), qgetenv(name)); - } - _unsetEnv(name); - } - - _setGstEnv("PYTHONNOUSERSITE", QStringLiteral("1")); -} - -void _restorePythonEnv() -{ - if (!s_pythonEnvSanitized) { - return; - } - s_pythonEnvSanitized = false; - - qCDebug(GStreamerLog) << "Restoring Python environment variables"; - - for (const char *name : kPythonEnvVars) { - const QByteArray key(name); - if (s_savedPythonEnv.contains(key)) { - qputenv(name, s_savedPythonEnv.value(key)); - qCDebug(GStreamerLog) << " " << name << "=" << s_savedPythonEnv.value(key); - } else { - _unsetEnv(name); + qunsetenv(name); + qCDebug(GStreamerLog) << " unset" << name; } } - - s_savedPythonEnv.clear(); } void _applyGstEnvVars(const QString &pluginDir, const QString &gioModDir, @@ -309,7 +277,6 @@ void _applyGstEnvVars(const QString &pluginDir, const QString &gioModDir, _clearManagedGstEnvVars(); _sanitizePythonEnvForScanner(); - // Force fresh registry scan since we're redirecting plugin paths to bundled locations _setGstEnv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", QStringLiteral("no")); _setGstEnvIfExists("GIO_EXTRA_MODULES", gioModDir); _setGstEnvIfExecutable("GST_PTP_HELPER_1_0", ptpPath); @@ -585,6 +552,71 @@ bool _verifyPlugins() return result; } +void _logDecoderRanks() +{ + GList *factories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!factories) { + qCDebug(GStreamerDecoderRanksLog) << "No video decoder factories found"; + return; + } + + factories = g_list_sort(factories, [](gconstpointer lhs, gconstpointer rhs) -> gint { + const guint lhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(lhs)); + const guint rhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(rhs)); + if (lhsRank != rhsRank) { + return (lhsRank > rhsRank) ? -1 : 1; + } + return g_strcmp0(gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(lhs)), + gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(rhs))); + }); + + qCDebug(GStreamerDecoderRanksLog) << "Video decoder ranks:"; + for (GList *node = factories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + GstPluginFeature *feature = GST_PLUGIN_FEATURE(factory); + const gchar *featureName = gst_plugin_feature_get_name(feature); + const guint rank = gst_plugin_feature_get_rank(feature); + const gchar *klass = gst_element_factory_get_klass(factory); + const bool isHw = GStreamer::isHardwareDecoderFactory(factory); + + GstPlugin *plugin = gst_plugin_feature_get_plugin(feature); + const gchar *pluginName = plugin ? gst_plugin_get_name(plugin) : "?"; + + qCDebug(GStreamerDecoderRanksLog).noquote() + << QStringLiteral(" [%1] %2/%3 rank=%4 (%5)") + .arg(isHw ? QStringLiteral("HW") : QStringLiteral("SW"), + QString::fromUtf8(pluginName), + QString::fromUtf8(featureName)) + .arg(rank) + .arg(QString::fromUtf8(klass)); + + if (plugin) { + gst_object_unref(plugin); + } + } + + gst_plugin_feature_list_free(factories); +} + +void _configureDebugLogging() +{ + gst_debug_remove_log_function(gst_debug_log_default); + gst_debug_add_log_function(GStreamer::qtGstLog, nullptr, nullptr); + + if (!qEnvironmentVariableIsEmpty("GST_DEBUG")) { + return; + } + + QSettings settings; + if (settings.contains(AppSettings::gstDebugLevelName)) { + const int level = qBound(0, settings.value(AppSettings::gstDebugLevelName).toInt(), + static_cast(GST_LEVEL_MEMDUMP)); + gst_debug_set_default_threshold(static_cast(level)); + } +} void prepareEnvironment() { @@ -617,11 +649,9 @@ bool _initGstRuntime() qCCritical(GStreamerLog) << "Failed to initialize GStreamer:" << (error ? error->message : "unknown error"); g_clear_error(&error); - _restorePythonEnv(); return false; } - _restorePythonEnv(); return true; } @@ -632,7 +662,7 @@ bool completeInit() return false; } - GStreamer::configureDebugLogging(); + _configureDebugLogging(); gchar *version = gst_version_string(); qCDebug(GStreamerLog) << "GStreamer initialized:" << version; @@ -645,16 +675,21 @@ bool completeInit() return false; } - GStreamer::logDecoderRanks(); - GStreamer::setCodecPriorities(static_cast( - SettingsManager::instance()->videoSettings()->forceVideoDecoder()->rawValue().toInt())); + _logDecoderRanks(); + + GstElementFactory *sinkFactory = gst_element_factory_find("qml6glsink"); + if (!sinkFactory) { + qCCritical(GStreamerLog) << "qml6glsink factory not found"; + return false; + } + gst_object_unref(sinkFactory); - GstElement *sink = gst_element_factory_make("qml6glsink", nullptr); - if (!sink) { - qCCritical(GStreamerLog) << "Failed to create qml6glsink element"; + GstElementFactory *playbinFactory = gst_element_factory_find("playbin"); + if (!playbinFactory) { + qCCritical(GStreamerLog) << "playbin factory not found"; return false; } - gst_clear_object(&sink); + gst_object_unref(playbinFactory); if (GStreamer::didExternalPluginLoaderFail()) { qCCritical(GStreamerLog) @@ -667,11 +702,8 @@ bool completeInit() bool initialize() { - prepareEnvironment(); - GStreamer::resetExternalPluginLoaderFailure(); GStreamer::redirectGLibLogging(); - GStreamer::configureDebugLogging(); if (!_initGstRuntime()) { return false; @@ -680,6 +712,18 @@ bool initialize() return completeInit(); } +QFuture initializeAsync() +{ + prepareEnvironment(); + return QtConcurrent::run(&initialize); +} + +// Ownership protocol for the video sink element: +// createVideoSink — returns a floating-ref element (refcount conceptually 1). +// startDecoding — calls gst_object_ref (sinks float, refcount=1). +// _ensureVideoSinkInPipeline — gst_object_ref (+1=2), gst_bin_add (+1=3). +// _shutdownDecodingBranch — gst_bin_remove (-1=2), gst_clear_object (-1=1). +// releaseVideoSink — gst_clear_object (-1=0, freed). void *createVideoSink(QQuickItem *widget, QObject * /*parent*/) { GstElement *videoSinkBin = gst_element_factory_make("qgcvideosinkbin", NULL); diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h index d20aaccaa9e8..6ca777db5914 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h @@ -1,5 +1,12 @@ #pragma once +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(GStreamerLog) +Q_DECLARE_LOGGING_CATEGORY(GStreamerAPILog) +Q_DECLARE_LOGGING_CATEGORY(GStreamerDecoderRanksLog) + class QQuickItem; class VideoReceiver; @@ -18,13 +25,8 @@ enum VideoDecoderOptions { ForceVideoDecoderHardware }; -// Call prepareEnvironment() on the main thread before dispatching initialize() -// to a worker thread. It sets process-wide env vars that gst_init reads. +QFuture initializeAsync(); void prepareEnvironment(); - -// Desktop: call initialize() which runs gst_init_check() + post-init steps. -// Android: Java calls gst_init(); call completeInit() after it succeeds. -// Both run on a worker thread — avoid accessing QObject singletons inside. bool initialize(); bool completeInit(); void *createVideoSink(QQuickItem *widget, QObject *parent = nullptr); diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc index d8717b6aa407..52f59412729f 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc @@ -1,8 +1,8 @@ #include "GStreamerHelpers.h" -#include "GStreamerLogging.h" #include #include +#include #include namespace GStreamer @@ -265,53 +265,4 @@ void setCodecPriorities(VideoDecoderOptions option) } } -void logDecoderRanks() -{ - GList *factories = gst_element_factory_list_get_elements( - static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), - GST_RANK_NONE); - - if (!factories) { - qCDebug(GStreamerDecoderRanksLog) << "No video decoder factories found"; - return; - } - - factories = g_list_sort(factories, [](gconstpointer lhs, gconstpointer rhs) -> gint { - const guint lhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(lhs)); - const guint rhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(rhs)); - if (lhsRank != rhsRank) { - return (lhsRank > rhsRank) ? -1 : 1; - } - return g_strcmp0(gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(lhs)), - gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(rhs))); - }); - - qCDebug(GStreamerDecoderRanksLog) << "Video decoder ranks:"; - for (GList *node = factories; node != nullptr; node = node->next) { - GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); - GstPluginFeature *feature = GST_PLUGIN_FEATURE(factory); - const gchar *featureName = gst_plugin_feature_get_name(feature); - const guint rank = gst_plugin_feature_get_rank(feature); - const gchar *klass = gst_element_factory_get_klass(factory); - const bool isHw = GStreamer::isHardwareDecoderFactory(factory); - - GstPlugin *plugin = gst_plugin_feature_get_plugin(feature); - const gchar *pluginName = plugin ? gst_plugin_get_name(plugin) : "?"; - - qCDebug(GStreamerDecoderRanksLog).noquote() - << QStringLiteral(" [%1] %2/%3 rank=%4 (%5)") - .arg(isHw ? QStringLiteral("HW") : QStringLiteral("SW"), - QString::fromUtf8(pluginName), - QString::fromUtf8(featureName)) - .arg(rank) - .arg(QString::fromUtf8(klass)); - - if (plugin) { - gst_object_unref(plugin); - } - } - - gst_plugin_feature_list_free(factories); -} - } // namespace GStreamer diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h index 7d807d73fe90..cd55babd41c9 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "GStreamer.h" @@ -10,7 +11,5 @@ namespace GStreamer bool isHardwareDecoderFactory(GstElementFactory *factory); - void logDecoderRanks(); - void setCodecPriorities(VideoDecoderOptions option); } diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc index 10db5ee87f64..81138e437b8b 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc @@ -1,8 +1,7 @@ +#include "GStreamer.h" #include "GStreamerLogging.h" -#include "AppSettings.h" #include "QGCLoggingCategory.h" -#include #include #include @@ -125,27 +124,4 @@ void qtGstLog(GstDebugCategory *category, } } -void configureDebugLogging() -{ - static bool s_configured = false; - if (s_configured) { - return; - } - s_configured = true; - - gst_debug_remove_log_function(gst_debug_log_default); - gst_debug_add_log_function(GStreamer::qtGstLog, nullptr, nullptr); - - if (!qEnvironmentVariableIsEmpty("GST_DEBUG")) { - return; - } - - QSettings settings; - if (settings.contains(AppSettings::gstDebugLevelName)) { - const int level = qBound(0, settings.value(AppSettings::gstDebugLevelName).toInt(), - static_cast(GST_LEVEL_MEMDUMP)); - gst_debug_set_default_threshold(static_cast(level)); - } -} - } // namespace GStreamer diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h index c1c205eccf1c..6a6212ede873 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h @@ -1,21 +1,13 @@ #pragma once -#include - #include -Q_DECLARE_LOGGING_CATEGORY(GStreamerLog) -Q_DECLARE_LOGGING_CATEGORY(GStreamerAPILog) -Q_DECLARE_LOGGING_CATEGORY(GStreamerDecoderRanksLog) - namespace GStreamer { void redirectGLibLogging(); void resetExternalPluginLoaderFailure(); bool didExternalPluginLoaderFail(); - void configureDebugLogging(); - void qtGstLog(GstDebugCategory *category, GstDebugLevel level, const gchar *file, diff --git a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc index 589faaf8446f..187f22aaaaaf 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc @@ -290,6 +290,7 @@ void GstVideoReceiver::stop() GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-stopped"); gst_clear_object(&_pipeline); + _pipeline = nullptr; _recorderValve = nullptr; _decoderValve = nullptr; @@ -370,7 +371,7 @@ void GstVideoReceiver::startDecoding(void *sink) if (!_addDecoder(_decoderValve)) { qCCritical(GstVideoReceiverLog) << "_addDecoder() failed" << _uri; - _cleanupVideoSinkOnFailure(); + _shutdownDecodingBranch(); _dispatchSignal([this]() { emit onStartDecodingComplete(STATUS_FAIL); }); return; } @@ -902,7 +903,7 @@ void GstVideoReceiver::_onNewSourcePad(GstPad *pad) if (!_addDecoder(_decoderValve)) { qCCritical(GstVideoReceiverLog) << "_addDecoder() failed"; - _cleanupVideoSinkOnFailure(); + _shutdownDecodingBranch(); return; } @@ -986,8 +987,8 @@ bool GstVideoReceiver::_addDecoder(GstElement *src) if (!gst_element_link(src, _decoder)) { qCCritical(GstVideoReceiverLog) << "Unable to link decoder"; - (void) gst_element_set_state(_decoder, GST_STATE_NULL); (void) gst_bin_remove(GST_BIN(_pipeline), _decoder); + (void) gst_element_set_state(_decoder, GST_STATE_NULL); gst_clear_object(&_decoder); return false; } @@ -1079,11 +1080,7 @@ bool GstVideoReceiver::_addVideoSink(GstPad *pad) // on QML's paint cycle). For live streams with hardware decoders that // have startup latency, this causes all frames to be silently dropped // after the first one. Override after state sync so it sticks. - // Re-apply the configured sync value since state sync may also reset it. - g_object_set(_videoSink, - "sync", (_buffer >= 0), - "max-lateness", G_GINT64_CONSTANT(-1), - nullptr); + g_object_set(_videoSink, "sync", FALSE, "max-lateness", G_GINT64_CONSTANT(-1), nullptr); GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-videosink"); @@ -1190,34 +1187,13 @@ bool GstVideoReceiver::_unlinkBranch(GstElement *from) return true; } -void GstVideoReceiver::_cleanupVideoSinkOnFailure() -{ - if (_videoSinkProbeId != 0) { - GstPad *sinkpad = gst_element_get_static_pad(_videoSink, "sink"); - if (sinkpad) { - gst_pad_remove_probe(sinkpad, _videoSinkProbeId); - gst_clear_object(&sinkpad); - } - _videoSinkProbeId = 0; - } - - GstObject *parent = gst_element_get_parent(_videoSink); - if (parent) { - (void) gst_element_set_state(_videoSink, GST_STATE_NULL); - (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink); - gst_clear_object(&parent); - } - - gst_clear_object(&_videoSink); -} - void GstVideoReceiver::_shutdownDecodingBranch() { if (_decoder) { GstObject *parent = gst_element_get_parent(_decoder); if (parent) { - (void) gst_element_set_state(_decoder, GST_STATE_NULL); (void) gst_bin_remove(GST_BIN(_pipeline), _decoder); + (void) gst_element_set_state(_decoder, GST_STATE_NULL); gst_clear_object(&parent); } @@ -1237,8 +1213,8 @@ void GstVideoReceiver::_shutdownDecodingBranch() GstObject *parent = gst_element_get_parent(_videoSink); if (parent) { - (void) gst_element_set_state(_videoSink, GST_STATE_NULL); (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink); + (void) gst_element_set_state(_videoSink, GST_STATE_NULL); gst_clear_object(&parent); } diff --git a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h index db9ff91a99dd..42870f6c1dd6 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h @@ -80,7 +80,6 @@ private slots: /// -Unlink the branch from the src pad /// -Send an EOS event at the beginning of that branch bool _unlinkBranch(GstElement *from); - void _cleanupVideoSinkOnFailure(); void _shutdownDecodingBranch(); void _shutdownRecordingBranch(); void _logDecodebin3SelectedCodec(GstElement *decodebin3); diff --git a/src/VideoManager/VideoReceiver/GStreamer/README.md b/src/VideoManager/VideoReceiver/GStreamer/README.md index 70e0405043ea..f3e497b20cb6 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/README.md +++ b/src/VideoManager/VideoReceiver/GStreamer/README.md @@ -16,8 +16,8 @@ QGC configures GStreamer runtime paths before `gst_init()`: - **Desktop (Linux/macOS/Windows)**: `GStreamer::prepareEnvironment()` sets plugin paths, scanner/helper paths, and GIO module paths when bundled runtimes are detected. - **Linux AppImage**: [`deploy/linux/AppRun`](../../../../deploy/linux/AppRun) exports `GST_PLUGIN_*`, `GST_PLUGIN_SCANNER*`, and `GST_PTP_HELPER*` only when bundled paths are valid. -- **Environment hygiene**: Python virtualenv/conda variables are temporarily cleared during `gst_init()` for scanner stability and restored afterwards. -- **Validation**: If bundled plugin directories are present but `gst-plugin-scanner` is missing or non-executable, a warning is logged and GStreamer falls back to in-process plugin scanning. +- **Environment hygiene**: Python virtualenv/conda variables are cleared for scanner stability (`PYTHONHOME`, `PYTHONPATH`, `VIRTUAL_ENV`, `CONDA_*`). +- **Validation**: If bundled plugin directories are present but `gst-plugin-scanner` is missing or non-executable, QGC fails initialization early with a clear error instead of running with a broken plugin loader. ## Platform Setup diff --git a/src/main.cc b/src/main.cc index 799cfff26e44..992b7eef1347 100644 --- a/src/main.cc +++ b/src/main.cc @@ -41,6 +41,10 @@ int main(int argc, char *argv[]) return QGCUnitTest::handleTestOptions(args); #endif case AppMode::BootTest: + if (!app.bootTestPassed()) { + qCCritical(MainLog) << "Simple boot test failed during GStreamer initialization"; + return 1; + } qCInfo(MainLog) << "Simple boot test completed"; return 0; case AppMode::Gui: diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 046f82d0ae5f..d492c7460a30 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -91,6 +91,12 @@ add_qgc_test(VehicleCameraControlTest LABELS Integration Vehicle RESOURCE_LOCK M add_qgc_test(QGCVideoStreamInfoTest LABELS Unit Camera) add_qgc_test(VideoManagerTest LABELS Unit) +# ---------------------------------------------------------------------------- +# VideoManager +# ---------------------------------------------------------------------------- +add_subdirectory(VideoManager) +add_qgc_test(GStreamerTest LABELS Integration) + # ---------------------------------------------------------------------------- # Comms # ---------------------------------------------------------------------------- diff --git a/test/VideoManager/CMakeLists.txt b/test/VideoManager/CMakeLists.txt new file mode 100644 index 000000000000..5b13ca02bc99 --- /dev/null +++ b/test/VideoManager/CMakeLists.txt @@ -0,0 +1,5 @@ +# ============================================================================ +# VideoManager Tests +# ============================================================================ + +add_subdirectory(GStreamer) diff --git a/test/VideoManager/GStreamer/CMakeLists.txt b/test/VideoManager/GStreamer/CMakeLists.txt new file mode 100644 index 000000000000..fb0a5e84e485 --- /dev/null +++ b/test/VideoManager/GStreamer/CMakeLists.txt @@ -0,0 +1,12 @@ +# ============================================================================ +# GStreamer Integration Tests +# Requires GStreamer runtime; tests skip gracefully when GST is disabled. +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + GStreamerTest.cc + GStreamerTest.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/test/VideoManager/GStreamer/GStreamerTest.cc b/test/VideoManager/GStreamer/GStreamerTest.cc new file mode 100644 index 000000000000..60194f0f5f99 --- /dev/null +++ b/test/VideoManager/GStreamer/GStreamerTest.cc @@ -0,0 +1,252 @@ +#include "GStreamerTest.h" + +#ifdef QGC_GST_STREAMING + +#include "GStreamer.h" +#include "GStreamerHelpers.h" +#include "GStreamerLogging.h" + +#include +#include +#include + +void GStreamerTest::init() +{ + UnitTest::init(); + + if (!gst_is_initialized()) { + GStreamer::prepareEnvironment(); + GStreamer::redirectGLibLogging(); + + GError *error = nullptr; + if (!gst_init_check(nullptr, nullptr, &error)) { + const QString msg = error ? QString::fromUtf8(error->message) : QStringLiteral("unknown error"); + g_clear_error(&error); + QSKIP(qPrintable(QStringLiteral("GStreamer unavailable: %1").arg(msg))); + } + } +} + +void GStreamerTest::_testIsValidRtspUri() +{ + QVERIFY(GStreamer::isValidRtspUri("rtsp://127.0.0.1:8554/test")); + QVERIFY(GStreamer::isValidRtspUri("rtsp://user:pass@10.0.0.1/stream")); + QVERIFY(GStreamer::isValidRtspUri("rtspu://192.168.1.1:554/video")); + QVERIFY(GStreamer::isValidRtspUri("rtspt://example.com/live")); + + QVERIFY(!GStreamer::isValidRtspUri(nullptr)); + QVERIFY(!GStreamer::isValidRtspUri("")); + QVERIFY(!GStreamer::isValidRtspUri("not-a-uri")); + QVERIFY(!GStreamer::isValidRtspUri("http://example.com")); + QVERIFY(!GStreamer::isValidRtspUri("udp://127.0.0.1:5600")); + QVERIFY(!GStreamer::isValidRtspUri("rtsp://")); +} + +void GStreamerTest::_testIsHardwareDecoderFactory() +{ + GList *factories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!factories) { + QSKIP("No video decoder factories available on this system"); + } + + int total = 0; + int hwCount = 0; + int swCount = 0; + + for (GList *node = factories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + QVERIFY(factory != nullptr); + + const gchar *name = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory)); + QVERIFY(name != nullptr); + + if (GStreamer::isHardwareDecoderFactory(factory)) { + ++hwCount; + } else { + ++swCount; + } + ++total; + } + + qCDebug(GStreamerLog) << "Decoder factory classification:" << total << "total," + << hwCount << "hardware," << swCount << "software"; + + QVERIFY(total > 0); + QVERIFY(swCount > 0); + QVERIFY(!GStreamer::isHardwareDecoderFactory(nullptr)); + + gst_plugin_feature_list_free(factories); +} + +void GStreamerTest::_testSetCodecPrioritiesDefault() +{ + GStreamer::setCodecPriorities(GStreamer::ForceVideoDecoderDefault); + + GstRegistry *registry = gst_registry_get(); + QVERIFY(registry != nullptr); +} + +void GStreamerTest::_testSetCodecPrioritiesSoftware() +{ + GStreamer::setCodecPriorities(GStreamer::ForceVideoDecoderSoftware); + + GList *factories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!factories) { + QSKIP("No video decoder factories available on this system"); + } + + bool foundPrioritizedSoftware = false; + for (GList *node = factories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + if (!factory) continue; + + if (!GStreamer::isHardwareDecoderFactory(factory)) { + const guint rank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory)); + if (rank > GST_RANK_MARGINAL) { + foundPrioritizedSoftware = true; + break; + } + } + } + + QVERIFY(foundPrioritizedSoftware); + + gst_plugin_feature_list_free(factories); +} + +void GStreamerTest::_testSetCodecPrioritiesHardware() +{ + GStreamer::setCodecPriorities(GStreamer::ForceVideoDecoderHardware); + + GList *factories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!factories) { + QSKIP("No video decoder factories available on this system"); + } + + for (GList *node = factories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + if (!factory) continue; + + if (!GStreamer::isHardwareDecoderFactory(factory)) { + const guint rank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory)); + QCOMPARE(rank, static_cast(GST_RANK_NONE)); + } + } + + gst_plugin_feature_list_free(factories); +} + +void GStreamerTest::_testRedirectGLibLogging() +{ + GStreamer::redirectGLibLogging(); + + g_log("TestDomain", G_LOG_LEVEL_DEBUG, "GStreamerTest debug message"); + g_log("TestDomain", G_LOG_LEVEL_WARNING, "GStreamerTest warning message"); +} + +void GStreamerTest::_testVerifyRequiredPlugins() +{ + GstRegistry *registry = gst_registry_get(); + QVERIFY(registry != nullptr); + + // coreelements comes from system/bundled GStreamer and should always be + // available after gst_init_check(). + GstPlugin *corePlugin = gst_registry_find_plugin(registry, "coreelements"); + QVERIFY2(corePlugin, "Required plugin not found: coreelements"); + gst_clear_object(&corePlugin); + + // qml6 and qgc are QGC-built static plugins only registered by + // _registerPlugins() during GStreamer::completeInit(). They aren't in the + // registry from a bare gst_init_check(), so just verify the registry + // loaded *some* plugins and that playbin is available (from system GStreamer). + GList *plugins = gst_registry_get_plugin_list(registry); + const int pluginCount = g_list_length(plugins); + gst_plugin_list_free(plugins); + QVERIFY2(pluginCount > 0, "GStreamer registry contains no plugins at all"); + + GstElementFactory *playbinFactory = gst_element_factory_find("playbin"); + QVERIFY2(playbinFactory, "Required factory not found: playbin"); + gst_object_unref(playbinFactory); +} + +void GStreamerTest::_testEnvironmentSetup() +{ + // Save and clear relevant env vars + struct EnvBackup { + const char *name; + QByteArray value; + bool wasSet; + }; + static constexpr const char *envVars[] = { + "GST_PLUGIN_PATH", "GST_PLUGIN_PATH_1_0", + "GST_PLUGIN_SYSTEM_PATH", "GST_PLUGIN_SYSTEM_PATH_1_0", + "GST_PLUGIN_SCANNER", "GST_PLUGIN_SCANNER_1_0", + "PYTHONHOME", "PYTHONPATH", "PYTHONUSERBASE", + "VIRTUAL_ENV", "CONDA_PREFIX", "CONDA_DEFAULT_ENV", + }; + QList backups; + for (const char *var : envVars) { + backups.append({var, qgetenv(var), qEnvironmentVariableIsSet(var)}); + qunsetenv(var); + } + + GStreamer::prepareEnvironment(); + + // On desktop builds with bundled plugins, env vars should be set. + // On dev builds without bundles, they remain unset (which is correct). + // Either way, if any plugin path is set, it should point to an existing directory. + for (const char *var : {"GST_PLUGIN_PATH", "GST_PLUGIN_PATH_1_0", + "GST_PLUGIN_SYSTEM_PATH", "GST_PLUGIN_SYSTEM_PATH_1_0"}) { + if (qEnvironmentVariableIsSet(var)) { + const QString path = qEnvironmentVariable(var); + // Paths may be colon-separated; check each component + const QStringList parts = path.split(QDir::listSeparator(), Qt::SkipEmptyParts); + for (const QString &part : parts) { + QVERIFY2(QDir(part).exists(), + qPrintable(QStringLiteral("%1 contains non-existent path: %2").arg(var, part))); + } + } + } + + for (const char *var : {"GST_PLUGIN_SCANNER", "GST_PLUGIN_SCANNER_1_0"}) { + if (qEnvironmentVariableIsSet(var)) { + const QString path = qEnvironmentVariable(var); + QVERIFY2(QFileInfo(path).isExecutable(), + qPrintable(QStringLiteral("%1 is not executable: %2").arg(var, path))); + } + } + + // Restore env vars + for (const EnvBackup &backup : backups) { + if (backup.wasSet) { + qputenv(backup.name, backup.value); + } else { + qunsetenv(backup.name); + } + } +} + +#else + +void GStreamerTest::init() { UnitTest::init(); QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testIsValidRtspUri() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testIsHardwareDecoderFactory() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testSetCodecPrioritiesDefault() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testSetCodecPrioritiesSoftware() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testSetCodecPrioritiesHardware() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testRedirectGLibLogging() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testVerifyRequiredPlugins() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testEnvironmentSetup() { QSKIP("GStreamer not enabled"); } + +#endif + +UT_REGISTER_TEST(GStreamerTest, TestLabel::Integration) diff --git a/test/VideoManager/GStreamer/GStreamerTest.h b/test/VideoManager/GStreamer/GStreamerTest.h new file mode 100644 index 000000000000..43f01e4a9ff3 --- /dev/null +++ b/test/VideoManager/GStreamer/GStreamerTest.h @@ -0,0 +1,20 @@ +#pragma once + +#include "UnitTest.h" + +class GStreamerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void init() override; + + void _testIsValidRtspUri(); + void _testIsHardwareDecoderFactory(); + void _testSetCodecPrioritiesDefault(); + void _testSetCodecPrioritiesSoftware(); + void _testSetCodecPrioritiesHardware(); + void _testRedirectGLibLogging(); + void _testVerifyRequiredPlugins(); + void _testEnvironmentSetup(); +}; diff --git a/tools/setup/build-gstreamer.py b/tools/setup/build-gstreamer.py index 0332e70d47ba..c881d33890b0 100755 --- a/tools/setup/build-gstreamer.py +++ b/tools/setup/build-gstreamer.py @@ -28,6 +28,7 @@ from __future__ import annotations import argparse +import json import os import platform import shutil @@ -38,29 +39,20 @@ from dataclasses import dataclass, field from pathlib import Path -try: - from .setup_bootstrap import ensure_setup_imports -except ImportError: - setup_dir = Path(__file__).resolve().parent - if str(setup_dir) not in sys.path: - sys.path.insert(0, str(setup_dir)) - from setup_bootstrap import ensure_setup_imports - -ensure_setup_imports() +_tools_dir = Path(__file__).resolve().parents[1] +if str(_tools_dir) not in sys.path: + sys.path.insert(0, str(_tools_dir)) from common.logging import log_info, log_ok, log_warn, log_error -from common.build_config import get_build_config_value -from common.gh_actions import write_github_output # ============================================================================ -# Build Utilities +# Shared Utilities # ============================================================================ MESON_VERSION = "1.10.1" NINJA_VERSION = "1.13.0" - def run_cmd(cmd: list, cwd: Path | None = None, check: bool = True, env: dict | None = None) -> subprocess.CompletedProcess: """Run a command with optional working directory and environment.""" @@ -95,6 +87,32 @@ def detect_host_arch() -> str: return 'armv7' return machine +def read_config(key: str, default: str = '') -> str: + """Read a value from build-config.json.""" + script_dir = Path(__file__).parent + config_file = script_dir.parent.parent / '.github' / 'build-config.json' + + if not config_file.exists(): + return default + + try: + with open(config_file) as f: + config = json.load(f) + return config.get(key, default) + except FileNotFoundError: + return default + except json.JSONDecodeError as error: + log_error(f"Failed to parse JSON config '{config_file}': {error}") + raise + +def write_github_output(outputs: dict) -> None: + """Write outputs for GitHub Actions.""" + github_output = os.environ.get('GITHUB_OUTPUT') + if github_output: + with open(github_output, 'a') as f: + for key, value in outputs.items(): + f.write(f"{key}={value}\n") + # ============================================================================ # Plugin Configurations @@ -604,11 +622,7 @@ def main() -> int: args = parse_args() # Resolve defaults - version = args.version or get_build_config_value( - 'gstreamer_default_version', - '1.24.13', - start=Path(__file__).resolve(), - ) + version = args.version or read_config('gstreamer_default_version', '1.24.13') arch = args.arch or get_default_arch(args.platform) prefix = Path(args.prefix) if args.prefix else None work_dir = Path(args.work_dir) diff --git a/tools/setup/download_artifacts.py b/tools/setup/download_artifacts.py index aca768b3fd73..3a3dc59a8db4 100755 --- a/tools/setup/download_artifacts.py +++ b/tools/setup/download_artifacts.py @@ -17,23 +17,19 @@ import argparse import concurrent.futures +from datetime import datetime import json +import subprocess import sys from pathlib import Path from typing import Any -try: - from .setup_bootstrap import ensure_setup_imports -except ImportError: - setup_dir = Path(__file__).resolve().parent - if str(setup_dir) not in sys.path: - sys.path.insert(0, str(setup_dir)) - from setup_bootstrap import ensure_setup_imports +REPO_ROOT = Path(__file__).resolve().parents[2] +COMMON_DIR = REPO_ROOT / "tools" / "common" +if str(COMMON_DIR) not in sys.path: + sys.path.insert(0, str(COMMON_DIR)) -ensure_setup_imports() - -from common.gh_actions import gh, list_workflow_runs_for_sha, list_run_artifacts -from common.github_runs import group_runs_by_name, select_latest_runs_by_name +import gh_actions as _gh_actions ARTIFACT_EXTENSIONS = ( @@ -48,6 +44,34 @@ ) ARTIFACT_EXTENSION_SET = frozenset(ARTIFACT_EXTENSIONS) + +def gh(*args: str, check: bool = True) -> subprocess.CompletedProcess: + """Run a gh CLI command and return the result.""" + return _gh_actions.gh(*args, check=check) + + +def _parse_created_at(created_at: Any) -> datetime | None: + value = str(created_at).strip() + if not value: + return None + if value.endswith("Z"): + value = f"{value[:-1]}+00:00" + try: + return datetime.fromisoformat(value) + except ValueError: + return None + + +def _is_newer_run(candidate: dict[str, Any], existing: dict[str, Any]) -> bool: + candidate_created_at = str(candidate.get("created_at", "")) + existing_created_at = str(existing.get("created_at", "")) + candidate_dt = _parse_created_at(candidate_created_at) + existing_dt = _parse_created_at(existing_created_at) + if candidate_dt is not None and existing_dt is not None: + return candidate_dt > existing_dt + return candidate_created_at > existing_created_at + + def select_latest_successful_runs( runs: list[dict[str, Any]], workflows: list[str], @@ -55,13 +79,21 @@ def select_latest_successful_runs( event: str = "", ) -> list[dict[str, Any]]: """Return latest completed/successful run per workflow name.""" - latest_by_workflow = select_latest_runs_by_name( - runs, - set(workflows), - event=event, - status="completed", - conclusion="success", - ) + workflow_set = set(workflows) + latest_by_workflow: dict[str, dict[str, Any]] = {} + + for run in runs: + name = str(run.get("name", "")) + if name not in workflow_set: + continue + if run.get("status") != "completed" or run.get("conclusion") != "success": + continue + if event and run.get("event") != event: + continue + existing = latest_by_workflow.get(name) + if existing is None or _is_newer_run(run, existing): + latest_by_workflow[name] = run + order = {name: idx for idx, name in enumerate(workflows)} return sorted( latest_by_workflow.values(), @@ -76,24 +108,40 @@ def group_successful_runs_by_workflow( event: str = "", ) -> dict[str, list[dict[str, Any]]]: """Group successful runs by workflow name, newest first.""" - return group_runs_by_name( - runs, - workflows, - event=event, - status="completed", - conclusion="success", - ) + workflow_set = set(workflows) + grouped: dict[str, list[dict[str, Any]]] = {name: [] for name in workflows} + + for run in runs: + name = str(run.get("name", "")) + if name not in workflow_set: + continue + if run.get("status") != "completed" or run.get("conclusion") != "success": + continue + if event and run.get("event") != event: + continue + grouped[name].append(run) + + for workflow_name in grouped: + grouped[workflow_name].sort( + key=lambda run: ( + _parse_created_at(run.get("created_at")) is not None, + str(run.get("created_at", "")), + ), + reverse=True, + ) + + return grouped def get_workflow_runs(repo: str, head_sha: str, workflows: list[str], *, event: str = "") -> list[dict]: """Return completed successful runs for the given workflows and commit.""" - all_runs = list_workflow_runs_for_sha(repo, head_sha) + all_runs = _gh_actions.list_workflow_runs_for_sha(repo, head_sha) return select_latest_successful_runs(all_runs, workflows, event=event) def select_artifact_names_for_run(repo: str, run_id: int, prefixes: list[str]) -> list[str]: """Return artifact names that match configured name prefixes.""" - return select_artifact_names(list_run_artifacts(repo, run_id), prefixes) + return select_artifact_names(_gh_actions.list_run_artifacts(repo, run_id), prefixes) def select_artifact_names(artifacts: list[dict[str, Any]], prefixes: list[str]) -> list[str]: @@ -232,7 +280,7 @@ def main(argv: list[str] | None = None) -> int: return 1 all_runs = loaded_runs else: - all_runs = list_workflow_runs_for_sha(repo, head_sha) + all_runs = _gh_actions.list_workflow_runs_for_sha(repo, head_sha) preloaded_artifacts: dict[int, list[dict[str, Any]]] = {} had_successful_runs = bool(select_latest_successful_runs(all_runs, workflows, event=event)) if artifact_prefixes: @@ -243,7 +291,7 @@ def main(argv: list[str] | None = None) -> int: selected_run: dict[str, Any] | None = None for candidate in candidates: run_id = int(candidate["id"]) - artifacts = list_run_artifacts(repo, run_id) + artifacts = _gh_actions.list_run_artifacts(repo, run_id) preloaded_artifacts[run_id] = artifacts if select_artifact_names(artifacts, artifact_prefixes): selected_run = candidate @@ -284,7 +332,7 @@ def main(argv: list[str] | None = None) -> int: if artifact_prefixes or artifact_metadata_out is not None: artifacts = preloaded_artifacts.get(run_id) if artifacts is None: - artifacts = list_run_artifacts(repo, run_id) + artifacts = _gh_actions.list_run_artifacts(repo, run_id) if artifact_prefixes: names = select_artifact_names(artifacts, artifact_prefixes) run_artifact_map[run_id] = names if names else None diff --git a/tools/setup/install_dependencies.py b/tools/setup/install_dependencies.py index cf769f222e05..2ac7575f20cf 100644 --- a/tools/setup/install_dependencies.py +++ b/tools/setup/install_dependencies.py @@ -26,19 +26,6 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path -try: - from .setup_bootstrap import ensure_setup_imports -except ImportError: - setup_dir = Path(__file__).resolve().parent - if str(setup_dir) not in sys.path: - sys.path.insert(0, str(setup_dir)) - from setup_bootstrap import ensure_setup_imports - -ensure_setup_imports() - -from common.file_traversal import find_repo_root -from common.build_config import load_build_config as load_shared_build_config - # Package categories for Debian/Ubuntu DEBIAN_PACKAGES: dict[str, list[str]] = { "core": [ @@ -172,16 +159,26 @@ PACKAGE_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9.+-]*$") +def get_repo_root() -> Path: + """Find repository root directory.""" + current = Path(__file__).resolve() + for parent in [current] + list(current.parents): + if (parent / ".git").exists(): + return parent + return Path.cwd() + + def load_build_config() -> dict: """Load build configuration from .github/build-config.json.""" - config_path = find_repo_root() / ".github" / "build-config.json" - if not config_path.exists(): - return {} - try: - return load_shared_build_config(config_path) - except (json.JSONDecodeError, OSError, ValueError) as e: - print(f"Error: invalid JSON in {config_path}: {e}", file=sys.stderr) - return {} + config_path = get_repo_root() / ".github" / "build-config.json" + if config_path.exists(): + try: + with open(config_path) as f: + return json.load(f) + except json.JSONDecodeError as e: + print(f"Error: invalid JSON in {config_path}: {e}", file=sys.stderr) + return {} + return {} def get_config_value(key: str) -> str | None: @@ -766,11 +763,6 @@ def parse_args(args: list[str] | None = None) -> argparse.Namespace: action="store_true", help="Print space-separated package list (machine-readable, for CI caching)", ) - parser.add_argument( - "--print-available-packages", - action="store_true", - help="Print available apt packages in the selected Debian category", - ) parser.add_argument( "--category", help="Install only specific category (Debian only)", @@ -794,6 +786,11 @@ def parse_args(args: list[str] | None = None) -> argparse.Namespace: action="store_true", help="Install Vulkan SDK (Windows only)", ) + parser.add_argument( + "--print-available-packages", + action="store_true", + help="Print available apt packages in the selected Debian category", + ) parser.add_argument( "--validate-extra-packages", nargs="*", diff --git a/tools/setup/install_python.py b/tools/setup/install_python.py index e591761cbe3d..cce6366e51fa 100755 --- a/tools/setup/install_python.py +++ b/tools/setup/install_python.py @@ -22,15 +22,9 @@ import tomllib from pathlib import Path -try: - from .setup_bootstrap import ensure_setup_imports -except ImportError: - setup_dir = Path(__file__).resolve().parent - if str(setup_dir) not in sys.path: - sys.path.insert(0, str(setup_dir)) - from setup_bootstrap import ensure_setup_imports - -ensure_setup_imports() +_tools_dir = Path(__file__).resolve().parents[1] +if str(_tools_dir) not in sys.path: + sys.path.insert(0, str(_tools_dir)) from common.file_traversal import find_repo_root diff --git a/tools/setup/read_config.py b/tools/setup/read_config.py index 5d8315199ad7..e9a90fe81114 100755 --- a/tools/setup/read_config.py +++ b/tools/setup/read_config.py @@ -26,22 +26,72 @@ from pathlib import Path from typing import Any -try: - from .setup_bootstrap import ensure_setup_imports -except ImportError: - setup_dir = Path(__file__).resolve().parent - if str(setup_dir) not in sys.path: - sys.path.insert(0, str(setup_dir)) - from setup_bootstrap import ensure_setup_imports -ensure_setup_imports() - -from common.build_config import ( - export_build_config_values, - find_build_config, - format_github_output, - load_build_config, -) +def find_config_file() -> Path: + """Find build-config.json by searching up from script location.""" + # Check for override + if env_path := os.environ.get("CONFIG_FILE"): + return Path(env_path) + + # Check script directory first (for Docker builds where config is copied) + script_dir = Path(__file__).parent + local_config = script_dir / "build-config.json" + if local_config.exists(): + return local_config + + # Search up for .github/build-config.json + current = script_dir.resolve() + while current != current.parent: + config_path = current / ".github" / "build-config.json" + if config_path.exists(): + return config_path + current = current.parent + + raise FileNotFoundError("Could not find build-config.json") + + +def load_config(config_file: Path) -> dict[str, Any]: + """Load and parse the JSON config file.""" + with open(config_file) as f: + return json.load(f) + + +# Keys to export as environment variables (top-level simple values) +EXPORT_KEYS = [ + "qt_version", + "qt_minimum_version", + "qt_modules", + "gstreamer_minimum_version", + "gstreamer_default_version", + "gstreamer_macos_version", + "gstreamer_ios_version", + "gstreamer_android_version", + "gstreamer_windows_version", + "xcode_version", + "xcode_ios_version", + "ndk_version", + "ndk_full_version", + "java_version", + "android_platform", + "android_min_sdk", + "android_build_tools", + "android_cmdline_tools", + "cmake_minimum_version", + "macos_deployment_target", + "ios_deployment_target", +] + + +def get_export_values(config: dict[str, Any]) -> dict[str, str]: + """Extract values to export, converting to uppercase env var names.""" + result = {} + for key in EXPORT_KEYS: + if key in config: + env_name = key.upper() + result[env_name] = str(config[key]) + if key == "gstreamer_default_version": + result["GSTREAMER_VERSION"] = str(config[key]) + return result def _escape_bash_value(value: str) -> str: @@ -74,6 +124,23 @@ def format_powershell_export(values: dict[str, str]) -> str: return "\n".join(lines) +def format_github_output(values: dict[str, str]) -> str: + """Format for GITHUB_OUTPUT file.""" + lines = [] + for key, value in sorted(values.items()): + # GitHub output uses lowercase keys + lines.append(f"{key.lower()}={value}") + + # Add derived values + if "QT_MODULES" in values: + ios_excluded = {"qtserialport", "qtscxml"} + modules = values["QT_MODULES"].split() + ios_modules = " ".join(m for m in modules if m not in ios_excluded) + lines.append(f"qt_modules_ios={ios_modules}") + + return "\n".join(lines) + + def main() -> int: parser = argparse.ArgumentParser( description="Read build configuration from .github/build-config.json" @@ -104,13 +171,9 @@ def main() -> int: if args.config: config_file = Path(args.config) else: - script_dir = Path(__file__).parent - config_file = find_build_config( - script_dir, - extra_candidates=[script_dir / "build-config.json"], - ) + config_file = find_config_file() - config = load_build_config(config_file) + config = load_config(config_file) except FileNotFoundError as e: print(f"Error: {e}", file=sys.stderr) return 1 @@ -121,13 +184,12 @@ def main() -> int: # Handle --get for single value if args.get: key = args.get.lower() # Normalize to lowercase - if key in config: - print(config[key]) - return 0 - # Legacy alias: gstreamer_version -> gstreamer_default_version if key == "gstreamer_version" and "gstreamer_default_version" in config: print(config["gstreamer_default_version"]) return 0 + if key in config: + print(config[key]) + return 0 else: print(f"Error: Key '{key}' not found in config", file=sys.stderr) print(f"Available keys: {', '.join(sorted(config.keys()))}", file=sys.stderr) @@ -139,7 +201,7 @@ def main() -> int: return 0 # Get export values - values = export_build_config_values(config) + values = get_export_values(config) # Handle --export for shell statements if args.export: diff --git a/tools/setup/resolve_gstreamer_config.py b/tools/setup/resolve_gstreamer_config.py deleted file mode 100644 index 6f71367976ef..000000000000 --- a/tools/setup/resolve_gstreamer_config.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -"""Resolve GStreamer and Qt versions for GitHub Actions.""" - -from __future__ import annotations - -import argparse -import sys -from pathlib import Path - -try: - from .setup_bootstrap import ensure_setup_imports -except ImportError: - setup_dir = Path(__file__).resolve().parent - if str(setup_dir) not in sys.path: - sys.path.insert(0, str(setup_dir)) - from setup_bootstrap import ensure_setup_imports - -ensure_setup_imports() - -from common.build_config import get_build_config_value -from common.gh_actions import write_github_output - -VERSION_KEYS = { - "macos": "gstreamer_macos_version", - "windows": "gstreamer_windows_version", - "android": "gstreamer_android_version", - "ios": "gstreamer_ios_version", -} - - -def resolve_version(platform_name: str, requested_version: str) -> str: - """Return explicit version override or platform-specific config version.""" - if requested_version: - return requested_version - version_key = VERSION_KEYS.get(platform_name, "gstreamer_default_version") - return get_build_config_value(version_key, start=Path(__file__).resolve()) - - -def parse_args(argv: list[str] | None = None) -> argparse.Namespace: - """Parse command-line arguments.""" - parser = argparse.ArgumentParser(description="Resolve GStreamer action config values.") - parser.add_argument("--platform", required=True, choices=["linux", "macos", "windows", "android", "ios"]) - parser.add_argument("--version", default="", help="Explicit GStreamer version override") - return parser.parse_args(argv) - - -def main(argv: list[str] | None = None) -> int: - """Resolve config values and emit GitHub outputs.""" - args = parse_args(argv) - outputs = { - "py_cmd": "python3", - "version": resolve_version(args.platform, args.version), - "qt_version": get_build_config_value("qt_version", start=Path(__file__).resolve()), - } - write_github_output(outputs) - print(f"Building GStreamer {outputs['version']} for {args.platform}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tools/setup/setup_bootstrap.py b/tools/setup/setup_bootstrap.py deleted file mode 100644 index 97ca816dd165..000000000000 --- a/tools/setup/setup_bootstrap.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Bootstrap helper for entrypoints under tools/setup.""" - -from __future__ import annotations - -import sys -from pathlib import Path - - -def ensure_setup_imports() -> Path: - """Ensure the parent tools directory is importable.""" - tools_dir = Path(__file__).resolve().parents[1] - if str(tools_dir) not in sys.path: - sys.path.insert(0, str(tools_dir)) - return tools_dir diff --git a/tools/tests/test_download_artifacts.py b/tools/tests/test_download_artifacts.py index 255225ee916b..0eabb4d67c9d 100644 --- a/tools/tests/test_download_artifacts.py +++ b/tools/tests/test_download_artifacts.py @@ -34,7 +34,7 @@ def test_get_workflow_runs_filters_by_name_status_and_conclusion() -> None: {"id": 4, "name": "Other", "status": "completed", "conclusion": "success"}, ] - with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux", "Windows"]) assert len(runs) == 1 @@ -67,7 +67,7 @@ def test_get_workflow_runs_keeps_latest_successful_run_per_workflow() -> None: }, ] - with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux", "Windows"]) by_name = {run["name"]: run["id"] for run in runs} @@ -81,7 +81,7 @@ def test_get_workflow_runs_filters_by_event() -> None: {"id": 11, "name": "Linux", "status": "completed", "conclusion": "success", "event": "pull_request"}, ] - with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux"], event="pull_request") assert [run["id"] for run in runs] == [11] @@ -116,7 +116,7 @@ def test_select_artifact_names_for_run_filters_by_prefix() -> None: {"name": "emulator-diagnostics-123"}, {"name": "QGroundControl-x86_64.AppImage"}, ] - with patch.object(mod, "list_run_artifacts", return_value=artifacts): + with patch.object(mod._gh_actions, "list_run_artifacts", return_value=artifacts): names = mod.select_artifact_names_for_run( "owner/repo", 42, @@ -138,7 +138,7 @@ def test_list_downloaded_artifacts_filters_extensions(tmp_path: Path) -> None: def test_main_returns_zero_when_no_runs_found() -> None: - with patch.object(mod, "list_workflow_runs_for_sha", return_value=[]), patch.object( + with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=[]), patch.object( mod, "select_latest_successful_runs", return_value=[], ): rc = mod.main(["--repo", "owner/repo", "--head-sha", "abc123"]) @@ -147,7 +147,7 @@ def test_main_returns_zero_when_no_runs_found() -> None: def test_main_returns_one_when_downloads_fail_and_no_files(tmp_path: Path) -> None: runs = [{"id": 42, "name": "Linux", "status": "completed", "conclusion": "success"}] - with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( mod, "download_run_artifacts", return_value=False, @@ -165,10 +165,10 @@ def test_main_returns_one_when_downloads_fail_and_no_files(tmp_path: Path) -> No def test_main_returns_two_when_no_artifacts_match_prefixes(tmp_path: Path) -> None: runs = [{"id": 42, "name": "Linux", "status": "completed", "conclusion": "success"}] - with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( - mod, "list_run_artifacts", return_value=[{"name": "unrelated-artifact", "size_in_bytes": 1}], + mod._gh_actions, "list_run_artifacts", return_value=[{"name": "unrelated-artifact", "size_in_bytes": 1}], ), patch.object( mod, "download_run_artifacts", return_value=True, ), patch.object( @@ -242,8 +242,8 @@ def _artifacts_for_run(repo: str, run_id: int) -> list[dict[str, object]]: downloaded = tmp_path / "coverage.xml" downloaded.write_text("", encoding="utf-8") - with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( - mod, "list_run_artifacts", side_effect=_artifacts_for_run, + with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + mod._gh_actions, "list_run_artifacts", side_effect=_artifacts_for_run, ), patch.object( mod, "download_run_artifacts", return_value=True, ) as download_mock, patch.object( @@ -273,10 +273,10 @@ def test_main_writes_artifact_metadata_file(tmp_path: Path) -> None: downloaded = tmp_path / "dummy.txt" downloaded.write_text("x", encoding="utf-8") - with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( - mod, "list_run_artifacts", return_value=artifacts, + mod._gh_actions, "list_run_artifacts", return_value=artifacts, ), patch.object( mod, "download_run_artifacts", return_value=True, ), patch.object( diff --git a/tools/tests/test_resolve_gstreamer_config.py b/tools/tests/test_resolve_gstreamer_config.py deleted file mode 100644 index 28273bf3d5fc..000000000000 --- a/tools/tests/test_resolve_gstreamer_config.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -"""Tests for tools/setup/resolve_gstreamer_config.py.""" - -from __future__ import annotations - -from unittest.mock import patch - -from setup.resolve_gstreamer_config import resolve_version - - -def test_resolve_version_prefers_explicit_override() -> None: - assert resolve_version("linux", "1.2.3") == "1.2.3" - - -def test_resolve_version_uses_platform_specific_key() -> None: - with patch("setup.resolve_gstreamer_config.get_build_config_value", return_value="9.9.9") as mock_get: - assert resolve_version("windows", "") == "9.9.9" - mock_get.assert_called_once() - From 48e3c9acf533b7f51c0193365c1d99832c6f2ffa Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Thu, 12 Mar 2026 04:47:30 -0400 Subject: [PATCH 079/119] Consolidate CI actions: convert bash to Python, deduplicate helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared helpers (tools/common/gh_actions.py): - write_github_output, write_step_summary, append_github_env - is_fork_pr, resolve_cache_policy New CI scripts (.github/scripts/): - cmake_helper.py: build, configure, ctest, detect-jobs - install_dependencies_helper.py: universe repo, BLAS repair, optional pkgs - detect_changes.py: platform-specific change detection - verify_executable.py: cross-platform binary verification - aws_upload.py: credential/artifact validation, S3 upload - docker_helper.py: input validation and retry logic - deploy_docs.py: branch sanitization and git deploy - android_sdk_helper.py: NDK path setup - attest_helper.py: SBOM attestation condition checks New tools: - install_qt.py: Qt installation with arch mapping and cache keys Migrated 6 scripts from local write_github_output to shared helper. Replaced stdlib urllib with httpx in ccache_helper. Replaced xml.etree with defusedxml in test_duration_report. Updated 11 composite actions to delegate logic to Python scripts. Standardized action inputs to kebab-case (upload, aws-upload). Fixed inline Python escape bug (${GITHUB_WORKSPACE} → os.environ). Added security-events: write to all platform workflows. Simplified .gitignore: build*/ → /build*/ (root-only). Added platform_workflows to build-config.json. Tests: 245 passing across all suites. --- .clang-format | 6 +- .github/CONTRIBUTING.md | 57 +---- .github/actions/attest-and-upload/action.yml | 59 +++++ .github/actions/attest-condition/action.yml | 55 ----- .github/actions/attest-sbom/action.yml | 49 ++-- .github/actions/aws-upload/action.yml | 88 +++---- .github/actions/build-config/action.yml | 3 + .github/actions/build-setup/action.yml | 14 +- .github/actions/cache/action.yml | 28 ++- .github/actions/cmake-build/action.yml | 63 +---- .github/actions/cmake-configure/action.yml | 48 ++-- .../actions/collect-artifact-sizes/action.yml | 29 ++- .github/actions/deploy-docs/action.yml | 50 ++++ .github/actions/detect-changes/action.yml | 113 +-------- .github/actions/docker/action.yml | 50 +--- .../actions/download-all-artifacts/action.yml | 35 +-- .github/actions/gstreamer/action.yml | 62 +++-- .../actions/install-dependencies/action.yml | 57 +---- .github/actions/qt-android/action.yml | 70 +++--- .github/actions/qt-install/action.yml | 86 +++++-- .github/actions/qt-ios/action.yml | 4 +- .github/actions/run-unit-tests/action.yml | 51 +--- .github/actions/upload/action.yml | 44 ++-- .github/actions/verify-executable/action.yml | 59 +---- .github/build-config.json | 1 + .github/copilot-instructions.md | 36 +++ .github/scripts/android_sdk_helper.py | 57 +++++ .github/scripts/attest_helper.py | 53 +++++ .github/scripts/aws_upload.py | 87 +++++++ .github/scripts/benchmark_runner.py | 47 ++-- .github/scripts/ccache_helper.py | 68 +++--- .github/scripts/check_baseline_ready.py | 33 +-- .github/scripts/cmake_helper.py | 207 +++++++++++++++++ .github/scripts/collect_artifact_sizes.py | 7 +- .github/scripts/collect_build_status.py | 40 +--- .github/scripts/deploy_docs.py | 68 ++++++ .github/scripts/detect_changes.py | 201 ++++++++++++++++ .github/scripts/docker_helper.py | 73 ++++++ .../scripts}/download_artifacts.py | 103 ++------- .../scripts/generate_build_results_comment.py | 212 +++++++---------- .github/scripts/gstreamer_archive.py | 28 ++- .../scripts/install_dependencies_helper.py | 148 ++++++++++++ .github/scripts/plan_docker_builds.py | 27 +-- .github/scripts/resolve_gstreamer_config.py | 55 +++++ .github/scripts/size_analysis.py | 25 +- .github/scripts/templates/build_results.md.j2 | 89 +++++++ .github/scripts/test_duration_report.py | 4 +- .github/scripts/tests/test_aws_upload.py | 70 ++++++ .../tests/test_check_baseline_ready.py | 4 +- .github/scripts/tests/test_cmake_helper.py | 40 ++++ .../tests/test_collect_build_status.py | 3 +- .github/scripts/tests/test_deploy_docs.py | 23 ++ .github/scripts/tests/test_detect_changes.py | 133 +++++++++++ .github/scripts/tests/test_docker_helper.py | 30 +++ .../scripts}/tests/test_download_artifacts.py | 28 +-- .../tests/test_resolve_gstreamer_config.py | 18 ++ .github/scripts/verify_executable.py | 64 ++++++ .github/workflows/_detect-changes.yml | 39 ++++ .github/workflows/analysis.yml | 32 +-- .github/workflows/android.yml | 55 ++--- .github/workflows/build-results.yml | 116 ++++++++-- .github/workflows/ci-scripts.yml | 2 +- .github/workflows/docker.yml | 2 + .github/workflows/docs_deploy.yml | 43 +--- .github/workflows/doxygen_deploy.yml | 38 +-- .github/workflows/ios.yml | 19 +- .github/workflows/linux.yml | 170 ++------------ .github/workflows/macos.yml | 53 ++--- .github/workflows/windows.yml | 62 ++--- .gitignore | 5 +- .pre-commit-config.yaml | 3 +- .qmlformat.ini | 2 +- .vscode/launch.default.json | 15 +- .vscode/tasks.default.json | 62 +++-- .yamllint.yml | 5 +- AGENTS.md | 97 ++++++++ Makefile | 14 +- cmake/find-modules/FindQGCGStreamer.cmake | 6 +- justfile | 2 +- pyrightconfig.json | 5 +- ruff.toml | 4 +- test/QmlControls/ObjectListModelBaseTest.cc | 2 - .../Tests/TestBaseClassesTest.h | 40 +--- tools/README.md | 4 +- tools/common/build_config.py | 1 + tools/common/gh_actions.py | 174 +++++++++----- tools/pre_commit.py | 55 ++--- tools/pyproject.toml | 2 + tools/run_tests.py | 24 +- tools/setup/install_qt.py | 217 ++++++++++++++++++ tools/tests/conftest.py | 11 + tools/tests/test_gh_actions.py | 158 +++++++++++-- tools/tests/test_install_qt.py | 95 ++++++++ tools/uv.lock | 54 +++++ 94 files changed, 3242 insertions(+), 1708 deletions(-) create mode 100644 .github/actions/attest-and-upload/action.yml delete mode 100644 .github/actions/attest-condition/action.yml create mode 100644 .github/actions/deploy-docs/action.yml create mode 100644 .github/scripts/android_sdk_helper.py create mode 100644 .github/scripts/attest_helper.py create mode 100644 .github/scripts/aws_upload.py create mode 100644 .github/scripts/cmake_helper.py create mode 100644 .github/scripts/deploy_docs.py create mode 100644 .github/scripts/detect_changes.py create mode 100644 .github/scripts/docker_helper.py rename {tools/setup => .github/scripts}/download_artifacts.py (80%) mode change 100755 => 100644 create mode 100644 .github/scripts/install_dependencies_helper.py create mode 100644 .github/scripts/resolve_gstreamer_config.py create mode 100644 .github/scripts/templates/build_results.md.j2 create mode 100644 .github/scripts/tests/test_aws_upload.py create mode 100644 .github/scripts/tests/test_cmake_helper.py create mode 100644 .github/scripts/tests/test_deploy_docs.py create mode 100644 .github/scripts/tests/test_detect_changes.py create mode 100644 .github/scripts/tests/test_docker_helper.py rename {tools => .github/scripts}/tests/test_download_artifacts.py (88%) create mode 100644 .github/scripts/tests/test_resolve_gstreamer_config.py create mode 100644 .github/scripts/verify_executable.py create mode 100644 .github/workflows/_detect-changes.yml create mode 100644 AGENTS.md create mode 100644 tools/setup/install_qt.py create mode 100644 tools/tests/conftest.py create mode 100644 tools/tests/test_install_qt.py diff --git a/.clang-format b/.clang-format index 658337b50c55..82bec87eac12 100644 --- a/.clang-format +++ b/.clang-format @@ -56,11 +56,11 @@ SpacesInSquareBrackets: false SortIncludes: CaseSensitive IncludeBlocks: Regroup IncludeCategories: - - Regex: '^' # Qt headers + - Regex: '^<[a-z].*>' # System/STL headers Priority: 1 - - Regex: '^<.*>' # System/STL headers + - Regex: '^' # Qt headers Priority: 2 - - Regex: '.*' # Project headers + - Regex: '.*' # Project headers Priority: 3 # Alignment diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b3ea2120847d..58932ef48a3f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -104,62 +104,7 @@ QGroundControl uses [Crowdin](https://crowdin.com/project/qgroundcontrol) for co ## Coding Standards -For the complete coding style guide with examples, see [CODING_STYLE.md](../CODING_STYLE.md). - -### C++ Guidelines - -- **Standard**: C++20 -- **Framework**: Qt 6 guidelines -- **Naming Conventions**: - - Classes: `PascalCase` - - Methods/functions: `camelCase` - - Private members: `_leadingUnderscore` - - Constants: `ALL_CAPS` or `kPascalCase` - -- **Always use braces** for if/else/for/while statements - - ```cpp - // Good - if (condition) { - doSomething(); - } - - // Bad - if (condition) doSomething(); - ``` - -- **Defensive coding**: - - Always null-check pointers before use - - Validate all inputs - - Use Q_ASSERT for debug-build development checks only (compiled out in release builds) - - Always use defensive error handling in production code paths (never rely on Q_ASSERT) - - Handle errors gracefully in production code - -- **Code formatting**: - - Run `clang-format` before committing - - Follow `.clang-format`, `.clang-tidy`, `.editorconfig` in repo root - - See `CodingStyle.h`, `CodingStyle.cc`, `CodingStyle.qml` for examples - - 4 spaces for indentation (no tabs) - -### QML Guidelines - -- Follow Qt QML coding conventions -- Use type annotations -- Prefer declarative over imperative code -- See `src/QmlControls/QGCButton.qml` for examples - -### Logging - -Use Qt logging categories: - -```cpp -Q_DECLARE_LOGGING_CATEGORY(MyComponentLog) -QGC_LOGGING_CATEGORY(MyComponentLog, "qgc.component.name") - -qCDebug(MyComponentLog) << "Debug message:" << value; -qCWarning(MyComponentLog) << "Warning message"; -qCCritical(MyComponentLog) << "Critical error"; -``` +Follow [CODING_STYLE.md](../CODING_STYLE.md) for naming, formatting, C++20 features, QML style, and logging conventions. Run `clang-format` and `pre-commit run` before committing. ### Architecture Patterns diff --git a/.github/actions/attest-and-upload/action.yml b/.github/actions/attest-and-upload/action.yml new file mode 100644 index 000000000000..1a86600caccc --- /dev/null +++ b/.github/actions/attest-and-upload/action.yml @@ -0,0 +1,59 @@ +name: Attest and Upload +description: Generate SBOM attestation and upload build artifact + +inputs: + artifact-name: + description: 'Artifact filename (e.g. QGroundControl.AppImage)' + required: true + package-name: + description: 'Package name for GitHub artifact' + required: true + scan-path: + description: 'Path to scan for SBOM (defaults to build dir)' + required: false + default: '' + subject-name: + description: 'SBOM subject name (defaults to package-name)' + required: false + default: '' + aws-role-arn: + description: 'AWS IAM role ARN for OIDC authentication' + required: false + default: '' + aws-key-id: + description: 'AWS access key ID' + required: false + default: '' + aws-secret-access-key: + description: 'AWS secret access key' + required: false + default: '' + aws-distribution-id: + description: 'AWS CloudFront distribution ID' + required: false + default: '' + upload-aws: + description: 'Upload artifact to AWS' + required: false + default: 'true' + +runs: + using: composite + steps: + - name: Attest Build with SBOM + uses: ./.github/actions/attest-sbom + with: + subject-path: ${{ runner.temp }}/build/${{ inputs.artifact-name }} + subject-name: ${{ inputs.subject-name || inputs.package-name }} + scan-path: ${{ inputs.scan-path || format('{0}/build', runner.temp) }} + + - name: Upload Build File + uses: ./.github/actions/upload + with: + artifact-name: ${{ inputs.artifact-name }} + package-name: ${{ inputs.package-name }} + aws-role-arn: ${{ inputs.aws-role-arn }} + aws-key-id: ${{ inputs.aws-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-distribution-id: ${{ inputs.aws-distribution-id }} + upload-aws: ${{ inputs.upload-aws }} diff --git a/.github/actions/attest-condition/action.yml b/.github/actions/attest-condition/action.yml deleted file mode 100644 index 50e34f613198..000000000000 --- a/.github/actions/attest-condition/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Attestation Condition Check -description: Check if attestation should be performed (shared logic for attest actions) - -inputs: - subject-path: - description: "Path to the artifact to attest" - required: true - condition: - description: "Additional condition (for example matrix.build_type == Release)" - required: false - default: "true" - -outputs: - skip: - description: "Whether to skip attestation (true/false)" - value: ${{ steps.check.outputs.skip }} - -runs: - using: composite - steps: - - name: Check attestation conditions - id: check - shell: bash - env: - EVENT_NAME: ${{ github.event_name }} - PR_REPO: ${{ github.event.pull_request.head.repo.full_name }} - THIS_REPO: ${{ github.repository }} - CONDITION: ${{ inputs.condition }} - SUBJECT_PATH: ${{ inputs.subject-path }} - run: | - set -euo pipefail - - # Skip for external PRs (no repo permissions for attestations). - if [[ "${EVENT_NAME}" == "pull_request" ]]; then - if [[ "${PR_REPO}" != "${THIS_REPO}" ]]; then - echo "skip=true" >> "${GITHUB_OUTPUT}" - echo "Skipping attestation for external PR" - exit 0 - fi - fi - - if [[ "${CONDITION}" != "true" ]]; then - echo "skip=true" >> "${GITHUB_OUTPUT}" - echo "Skipping attestation: condition not met" - exit 0 - fi - - if [[ ! -e "${SUBJECT_PATH}" ]]; then - echo "skip=true" >> "${GITHUB_OUTPUT}" - echo "::warning::Artifact not found: ${SUBJECT_PATH}" - exit 0 - fi - - echo "skip=false" >> "${GITHUB_OUTPUT}" - echo "Will attest: ${SUBJECT_PATH}" diff --git a/.github/actions/attest-sbom/action.yml b/.github/actions/attest-sbom/action.yml index 28d16c03689b..4c480ef32bb6 100644 --- a/.github/actions/attest-sbom/action.yml +++ b/.github/actions/attest-sbom/action.yml @@ -25,52 +25,29 @@ outputs: runs: using: composite steps: - - name: Check attestation conditions + - name: Check attestation conditions and resolve paths id: check - uses: ./.github/actions/attest-condition - with: - subject-path: ${{ inputs.subject-path }} - - - name: Determine scan path - id: paths - if: steps.check.outputs.skip != 'true' shell: bash env: - SUBJECT_PATH: ${{ inputs.subject-path }} - SCAN_PATH: ${{ inputs.scan-path }} - SBOM_FORMAT: ${{ inputs.sbom-format }} - RUNNER_TEMP_DIR: ${{ runner.temp }} - SUBJECT_NAME: ${{ inputs.subject-name }} + EVENT_NAME: ${{ github.event_name }} + PR_REPO: ${{ github.event.pull_request.head.repo.full_name }} + THIS_REPO: ${{ github.repository }} run: | - set -euo pipefail - - subject_path="${SUBJECT_PATH}" - scan_path="${SCAN_PATH}" - - if [[ -z "${scan_path}" ]]; then - scan_path="$(dirname "${subject_path}")" - fi - - if [[ "${SBOM_FORMAT}" == "cyclonedx-json" ]]; then - suffix="cdx.json" - else - suffix="spdx.json" - fi - sbom_path="${RUNNER_TEMP_DIR}/${SUBJECT_NAME}.sbom.${suffix}" - - echo "scan-path=${scan_path}" >> "${GITHUB_OUTPUT}" - echo "sbom-path=${sbom_path}" >> "${GITHUB_OUTPUT}" - echo "Scan path: ${scan_path}" - echo "SBOM path: ${sbom_path}" + python3 "${GITHUB_WORKSPACE}/.github/scripts/attest_helper.py" \ + --subject-path "${{ inputs.subject-path }}" \ + --subject-name "${{ inputs.subject-name }}" \ + --scan-path "${{ inputs.scan-path }}" \ + --sbom-format "${{ inputs.sbom-format }}" \ + --runner-temp "${{ runner.temp }}" - name: Generate SBOM id: sbom if: steps.check.outputs.skip != 'true' uses: anchore/sbom-action@v0 with: - path: ${{ steps.paths.outputs.scan-path }} + path: ${{ steps.check.outputs.scan-path }} format: ${{ inputs.sbom-format }} - output-file: ${{ steps.paths.outputs.sbom-path }} + output-file: ${{ steps.check.outputs.sbom-path }} upload-artifact: true upload-artifact-retention: 30 artifact-name: sbom-${{ inputs.subject-name }} @@ -86,4 +63,4 @@ runs: uses: actions/attest-sbom@v3 with: subject-path: ${{ inputs.subject-path }} - sbom-path: ${{ steps.paths.outputs.sbom-path }} + sbom-path: ${{ steps.check.outputs.sbom-path }} diff --git a/.github/actions/aws-upload/action.yml b/.github/actions/aws-upload/action.yml index 41eaa952242a..a6d2c33b6aa0 100644 --- a/.github/actions/aws-upload/action.yml +++ b/.github/actions/aws-upload/action.yml @@ -2,29 +2,29 @@ name: AWS Upload description: Upload release artifacts to AWS S3 and invalidate CloudFront cache inputs: - artifact_name: + artifact-name: description: Artifact filename to upload required: true - artifact_path: + artifact-path: description: Path to artifact file required: true - aws_key_id: + aws-key-id: description: AWS access key ID required: false - aws_secret_access_key: + aws-secret-access-key: description: AWS secret access key required: false - aws_role_arn: + aws-role-arn: description: AWS IAM role ARN for OIDC authentication (preferred over static credentials) required: false - aws_distribution_id: + aws-distribution-id: description: AWS CloudFront distribution ID (optional, for cache invalidation) required: false - aws_region: + aws-region: description: AWS region required: false default: 'us-west-2' - s3_bucket: + s3-bucket: description: S3 bucket name required: false default: 'qgroundcontrol' @@ -32,78 +32,62 @@ inputs: runs: using: composite steps: - - name: Validate credentials + - name: Validate credentials and artifact shell: bash env: - AWS_ROLE_ARN: ${{ inputs.aws_role_arn }} - AWS_KEY_ID: ${{ inputs.aws_key_id }} - AWS_SECRET_KEY: ${{ inputs.aws_secret_access_key }} + AWS_ROLE_ARN: ${{ inputs.aws-role-arn }} + AWS_KEY_ID: ${{ inputs.aws-key-id }} + AWS_SECRET_KEY: ${{ inputs.aws-secret-access-key }} run: | - if [[ -z "$AWS_ROLE_ARN" && ( -z "$AWS_KEY_ID" || -z "$AWS_SECRET_KEY" ) ]]; then - echo "::error::Either aws_role_arn (OIDC) or both aws_key_id and aws_secret_access_key (static credentials) must be provided" - exit 1 - fi + python3 "${GITHUB_WORKSPACE}/.github/scripts/aws_upload.py" validate \ + --role-arn "${AWS_ROLE_ARN:-}" \ + --key-id "${AWS_KEY_ID:-}" \ + --secret-key "${AWS_SECRET_KEY:-}" \ + --artifact-path "${{ inputs.artifact-path }}" \ + --artifact-name "${{ inputs.artifact-name }}" - name: Configure AWS Credentials (OIDC) - if: inputs.aws_role_arn != '' + if: inputs.aws-role-arn != '' uses: aws-actions/configure-aws-credentials@v6 with: - role-to-assume: ${{ inputs.aws_role_arn }} - aws-region: ${{ inputs.aws_region }} + role-to-assume: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} - name: Configure AWS Credentials (Static) - if: inputs.aws_role_arn == '' && inputs.aws_key_id != '' + if: inputs.aws-role-arn == '' && inputs.aws-key-id != '' uses: aws-actions/configure-aws-credentials@v6 with: - aws-access-key-id: ${{ inputs.aws_key_id }} - aws-secret-access-key: ${{ inputs.aws_secret_access_key }} - aws-region: ${{ inputs.aws_region }} + aws-access-key-id: ${{ inputs.aws-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} - name: Upload to S3 builds folder shell: bash - env: - ARTIFACT_PATH: ${{ inputs.artifact_path }} - S3_BUCKET: ${{ inputs.s3_bucket }} - REF_NAME: ${{ github.ref_name }} - ARTIFACT_NAME: ${{ inputs.artifact_name }} run: | - # Validate artifact path exists and is a file - if [[ ! -f "$ARTIFACT_PATH" ]]; then - echo "Error: Artifact not found: $ARTIFACT_PATH" >&2 - exit 1 - fi - - # Validate artifact name contains only safe characters (no path separators) - if [[ "$ARTIFACT_NAME" =~ [/\\] ]] || [[ "$ARTIFACT_NAME" =~ \.\. ]]; then - echo "Error: Invalid artifact name (contains path separators): $ARTIFACT_NAME" >&2 - exit 1 - fi - - # Sanitize ref name - remove path traversal attempts and special chars - SAFE_REF_NAME=$(echo "$REF_NAME" | sed 's/\.\.//g' | sed 's/[^a-zA-Z0-9._-]/_/g') - # Intentional: public-read for distributing release builds to users. - aws s3 cp "$ARTIFACT_PATH" \ - "s3://${S3_BUCKET}/builds/${SAFE_REF_NAME}/${ARTIFACT_NAME}" \ - --acl public-read + python3 "${GITHUB_WORKSPACE}/.github/scripts/aws_upload.py" upload \ + --artifact-path "${{ inputs.artifact-path }}" \ + --artifact-name "${{ inputs.artifact-name }}" \ + --ref-name "${{ github.ref_name }}" \ + --s3-bucket "${{ inputs.s3-bucket }}" - name: Upload to S3 latest folder if: github.ref_type == 'tag' shell: bash env: - ARTIFACT_PATH: ${{ inputs.artifact_path }} - S3_BUCKET: ${{ inputs.s3_bucket }} - ARTIFACT_NAME: ${{ inputs.artifact_name }} + ARTIFACT_PATH: ${{ inputs.artifact-path }} + S3_BUCKET: ${{ inputs.s3-bucket }} + ARTIFACT_NAME: ${{ inputs.artifact-name }} run: | aws s3 cp "$ARTIFACT_PATH" \ "s3://${S3_BUCKET}/latest/${ARTIFACT_NAME}" \ --acl public-read - name: Invalidate CloudFront cache - if: github.ref_type == 'tag' && inputs.aws_distribution_id != '' + if: github.ref_type == 'tag' && inputs.aws-distribution-id != '' shell: bash env: - DISTRIBUTION_ID: ${{ inputs.aws_distribution_id }} - ARTIFACT_NAME: ${{ inputs.artifact_name }} + DISTRIBUTION_ID: ${{ inputs.aws-distribution-id }} + ARTIFACT_NAME: ${{ inputs.artifact-name }} run: | aws cloudfront create-invalidation \ --distribution-id "$DISTRIBUTION_ID" \ diff --git a/.github/actions/build-config/action.yml b/.github/actions/build-config/action.yml index 91abbb710785..4fc098481acb 100644 --- a/.github/actions/build-config/action.yml +++ b/.github/actions/build-config/action.yml @@ -53,6 +53,9 @@ outputs: ios_deployment_target: description: iOS deployment target version value: ${{ steps.read.outputs.ios_deployment_target }} + platform_workflows: + description: Comma-separated platform workflow names + value: ${{ steps.read.outputs.platform_workflows }} runs: using: composite diff --git a/.github/actions/build-setup/action.yml b/.github/actions/build-setup/action.yml index 70b310a61ecf..2d5299b2175d 100644 --- a/.github/actions/build-setup/action.yml +++ b/.github/actions/build-setup/action.yml @@ -25,14 +25,23 @@ inputs: required: false default: "Release" save-cache: - description: Whether to save cache (typically false for PRs) + description: "Whether to save cache (auto = save for pushes and same-repo PRs, skip for fork PRs)" required: false - default: "false" + default: "auto" outputs: gstreamer_windows_version: description: GStreamer version for Windows value: ${{ steps.config.outputs.gstreamer_windows_version }} + qt_modules: + description: Qt modules used for this build + value: ${{ inputs.qt-modules || steps.config.outputs.qt_modules }} + qt_root_dir: + description: Installed Qt root directory + value: ${{ steps.qt.outputs.qt_root_dir }} + qt_version: + description: Resolved Qt version for this build + value: ${{ inputs.qt-version || steps.config.outputs.qt_version }} xcode_version: description: Xcode version for macOS value: ${{ steps.config.outputs.xcode_version }} @@ -61,6 +70,7 @@ runs: save-cache: ${{ inputs.save-cache }} - name: Install Qt + id: qt uses: ./.github/actions/qt-install with: version: ${{ inputs.qt-version || steps.config.outputs.qt_version }} diff --git a/.github/actions/cache/action.yml b/.github/actions/cache/action.yml index 712356f2df02..7fbd95ce13f2 100644 --- a/.github/actions/cache/action.yml +++ b/.github/actions/cache/action.yml @@ -14,12 +14,28 @@ inputs: description: Path to CPM Modules required: false save-cache: - description: Save cache (set to false for PR builds to save storage) + description: "Whether to save cache (auto = save for pushes and same-repo PRs, skip for fork PRs)" required: false - default: 'true' + default: 'auto' runs: using: composite steps: + - name: Resolve save-cache + id: cache-policy + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + PR_REPO: ${{ github.event.pull_request.head.repo.full_name }} + THIS_REPO: ${{ github.repository }} + run: | + save="$(python3 -c " + import os, sys + sys.path.insert(0, os.path.join(os.environ['GITHUB_WORKSPACE'], 'tools')) + from common.gh_actions import resolve_cache_policy + print(resolve_cache_policy('${{ inputs.save-cache }}')) + ")" + echo "save=${save}" >> "$GITHUB_OUTPUT" + - name: Determine cache scope id: cache-scope shell: bash @@ -117,7 +133,7 @@ runs: run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" fingerprint --root "${GITHUB_WORKSPACE}" - name: Restore Build Cache (ccache, save) - if: inputs.save-cache == 'true' + if: steps.cache-policy.outputs.save == 'true' uses: actions/cache@v5 with: path: ${{ github.workspace }}/.ccache @@ -128,7 +144,7 @@ runs: ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}- - name: Restore Build Cache (ccache, read-only) - if: inputs.save-cache != 'true' + if: steps.cache-policy.outputs.save != 'true' uses: actions/cache/restore@v5 with: path: ${{ github.workspace }}/.ccache @@ -143,7 +159,7 @@ runs: run: ccache -z - name: Cache CPM Modules (restore and save) - if: inputs.cpm-modules != '' && inputs.save-cache == 'true' + if: inputs.cpm-modules != '' && steps.cache-policy.outputs.save == 'true' uses: actions/cache@v5 with: path: ${{ steps.cpm-cache.outputs.path }} @@ -154,7 +170,7 @@ runs: cpm-modules- - name: Cache CPM Modules (restore only) - if: inputs.cpm-modules != '' && inputs.save-cache != 'true' + if: inputs.cpm-modules != '' && steps.cache-policy.outputs.save != 'true' uses: actions/cache/restore@v5 with: path: ${{ steps.cpm-cache.outputs.path }} diff --git a/.github/actions/cmake-build/action.yml b/.github/actions/cmake-build/action.yml index 3b0eb0c4744d..5f05d3490aac 100644 --- a/.github/actions/cmake-build/action.yml +++ b/.github/actions/cmake-build/action.yml @@ -54,60 +54,15 @@ runs: CONTINUE: ${{ inputs.continue-on-error }} REVIEWDOG: ${{ inputs.reviewdog }} run: | - set -euo pipefail - - # Build command - cmd=(cmake --build .) - - # Add target - [[ -n "$TARGET" ]] && cmd+=(--target "$TARGET") - - # Add config for multi-config generators - [[ -n "$BUILD_TYPE" ]] && cmd+=(--config "$BUILD_TYPE") - - # Add parallel flag - if [[ "$PARALLEL" == "true" ]]; then - if [[ -n "$PARALLEL_JOBS" ]]; then - if [[ ! "$PARALLEL_JOBS" =~ ^[1-9][0-9]*$ ]]; then - echo "::error::parallel-jobs must be a positive integer, got '$PARALLEL_JOBS'" - exit 1 - fi - cmd+=(--parallel "$PARALLEL_JOBS") - else - cmd+=(--parallel) - fi - fi - - # When reviewdog is enabled, force output capture for warning analysis - if [[ "$REVIEWDOG" == "true" && -z "$OUTPUT_FILE" ]]; then - OUTPUT_FILE="${RUNNER_TEMP:-.}/build-output.log" - echo "REVIEWDOG_LOG=${OUTPUT_FILE}" >> "$GITHUB_ENV" - elif [[ "$REVIEWDOG" == "true" ]]; then - echo "REVIEWDOG_LOG=${OUTPUT_FILE}" >> "$GITHUB_ENV" - fi - - echo "Running: ${cmd[*]}" - start_time=$(date +%s) - - set +e - if [[ -n "$OUTPUT_FILE" ]]; then - "${cmd[@]}" 2>&1 | tee "$OUTPUT_FILE" - exit_code=${PIPESTATUS[0]} - else - "${cmd[@]}" - exit_code=$? - fi - set -e - - end_time=$(date +%s) - duration=$((end_time - start_time)) - - if [[ $exit_code -eq 0 ]]; then - echo "✓ Build completed in ${duration}s" - else - echo "::error::Build failed after ${duration}s" - [[ "$CONTINUE" != "true" ]] && exit $exit_code - fi + ARGS=(build) + [[ -n "$TARGET" ]] && ARGS+=(--target "$TARGET") + [[ -n "$BUILD_TYPE" ]] && ARGS+=(--build-type "$BUILD_TYPE") + [[ "$PARALLEL" == "true" ]] && ARGS+=(--parallel) + [[ -n "$PARALLEL_JOBS" ]] && ARGS+=(--parallel-jobs "$PARALLEL_JOBS") + [[ -n "$OUTPUT_FILE" ]] && ARGS+=(--output-file "$OUTPUT_FILE") + [[ "$CONTINUE" == "true" ]] && ARGS+=(--continue-on-error) + [[ "$REVIEWDOG" == "true" ]] && ARGS+=(--reviewdog) + python3 "${GITHUB_WORKSPACE}/.github/scripts/cmake_helper.py" "${ARGS[@]}" - name: Setup reviewdog if: inputs.reviewdog == 'true' && env.REVIEWDOG_LOG != '' diff --git a/.github/actions/cmake-configure/action.yml b/.github/actions/cmake-configure/action.yml index 3e94b63f6aff..27daf1755063 100644 --- a/.github/actions/cmake-configure/action.yml +++ b/.github/actions/cmake-configure/action.yml @@ -58,41 +58,19 @@ runs: - name: Configure shell: bash - env: - INPUT_SOURCE_DIR: ${{ inputs.source-dir }} - INPUT_BUILD_DIR: ${{ inputs.build-dir }} - INPUT_GENERATOR: ${{ inputs.generator }} - INPUT_BUILD_TYPE: ${{ inputs.build-type }} - INPUT_TESTING: ${{ inputs.testing }} - INPUT_COVERAGE: ${{ inputs.coverage }} - INPUT_STABLE: ${{ inputs.stable }} - INPUT_USE_QT_CMAKE: ${{ inputs.use-qt-cmake }} - INPUT_UNITY_BUILD: ${{ inputs.unity-build }} - INPUT_UNITY_BATCH_SIZE: ${{ inputs.unity-batch-size }} - INPUT_EXTRA_ARGS: ${{ inputs.extra-args }} - WORKSPACE: ${{ github.workspace }} run: | - args=( - -S "${INPUT_SOURCE_DIR}" - -B "${INPUT_BUILD_DIR}" - -G "${INPUT_GENERATOR}" - -t "${INPUT_BUILD_TYPE}" + ARGS=(configure + --source-dir "${{ inputs.source-dir }}" + --build-dir "${{ inputs.build-dir }}" + --generator "${{ inputs.generator }}" + --build-type "${{ inputs.build-type }}" ) - - [[ "${INPUT_TESTING}" == "true" ]] && args+=(--testing) - [[ "${INPUT_COVERAGE}" == "true" ]] && args+=(--coverage) - [[ "${INPUT_STABLE}" == "true" ]] && args+=(--stable) - [[ "${INPUT_USE_QT_CMAKE}" != "true" ]] && args+=(--no-qt-cmake) - - if [[ "${INPUT_UNITY_BUILD}" == "true" ]]; then - args+=(--unity --unity-batch "${INPUT_UNITY_BATCH_SIZE}") + [[ "${{ inputs.testing }}" == "true" ]] && ARGS+=(--testing) + [[ "${{ inputs.coverage }}" == "true" ]] && ARGS+=(--coverage) + [[ "${{ inputs.stable }}" == "true" ]] && ARGS+=(--stable) + [[ "${{ inputs.use-qt-cmake }}" != "true" ]] && ARGS+=(--no-qt-cmake) + if [[ "${{ inputs.unity-build }}" == "true" ]]; then + ARGS+=(--unity-build --unity-batch-size "${{ inputs.unity-batch-size }}") fi - - # Add extra args if provided (after -- so argparse treats -D flags as positional) - if [[ -n "$INPUT_EXTRA_ARGS" ]]; then - read -ra extra <<< "$INPUT_EXTRA_ARGS" - args+=(--) - args+=("${extra[@]}") - fi - - python3 "${WORKSPACE}/tools/configure.py" "${args[@]}" + [[ -n "${{ inputs.extra-args }}" ]] && ARGS+=("--extra-args=${{ inputs.extra-args }}") + python3 "${GITHUB_WORKSPACE}/.github/scripts/cmake_helper.py" "${ARGS[@]}" diff --git a/.github/actions/collect-artifact-sizes/action.yml b/.github/actions/collect-artifact-sizes/action.yml index 9118c7b0e29c..761e977432b9 100644 --- a/.github/actions/collect-artifact-sizes/action.yml +++ b/.github/actions/collect-artifact-sizes/action.yml @@ -43,14 +43,21 @@ runs: RUNS_FILE: ${{ inputs.runs-file }} ARTIFACTS_FILE: ${{ inputs.artifacts-file }} run: | - ARGS=(--repo "${REPO}" --head-sha "${HEAD_SHA}" --platform-workflows "${WORKFLOWS}" --output-file "${OUTPUT_FILE}") - if [[ -n "${EVENT}" ]]; then - ARGS+=(--event "${EVENT}") - fi - if [[ -n "${RUNS_FILE}" && -f "${RUNS_FILE}" ]]; then - ARGS+=(--runs-file "${RUNS_FILE}") - fi - if [[ -n "${ARTIFACTS_FILE}" && -f "${ARTIFACTS_FILE}" ]]; then - ARGS+=(--artifacts-file "${ARTIFACTS_FILE}") - fi - python3 "${GITHUB_WORKSPACE}/.github/scripts/collect_artifact_sizes.py" "${ARGS[@]}" + python3 -c " + import os, subprocess, sys + args = ['${GITHUB_WORKSPACE}/.github/scripts/collect_artifact_sizes.py', + '--repo', os.environ['REPO'], + '--head-sha', os.environ['HEAD_SHA'], + '--platform-workflows', os.environ['WORKFLOWS'], + '--output-file', os.environ['OUTPUT_FILE']] + event = os.environ.get('EVENT', '') + if event: + args += ['--event', event] + runs = os.environ.get('RUNS_FILE', '') + if runs and os.path.isfile(runs): + args += ['--runs-file', runs] + artifacts = os.environ.get('ARTIFACTS_FILE', '') + if artifacts and os.path.isfile(artifacts): + args += ['--artifacts-file', artifacts] + sys.exit(subprocess.run([sys.executable] + args).returncode) + " diff --git a/.github/actions/deploy-docs/action.yml b/.github/actions/deploy-docs/action.yml new file mode 100644 index 000000000000..4611228167e6 --- /dev/null +++ b/.github/actions/deploy-docs/action.yml @@ -0,0 +1,50 @@ +name: Deploy Docs +description: Deploy built documentation to an external GitHub Pages repository + +inputs: + artifact-name: + description: Name of the uploaded artifact to download + required: true + target-repo: + description: 'Target repository in owner/repo format' + required: true + target-branch: + description: 'Branch to push to in target repo' + required: false + default: main + deploy-token: + description: 'GitHub token with push access to target repo' + required: true + source-branch: + description: 'Source branch name (used as subdirectory in target repo)' + required: true + commit-message: + description: 'Commit message prefix' + required: false + default: 'Docs update' + +runs: + using: composite + steps: + - name: Download artifact + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.artifact-name }} + path: ~/_docs_build + + - name: Checkout target repo + uses: actions/checkout@v6 + with: + repository: ${{ inputs.target-repo }} + token: ${{ inputs.deploy-token }} + path: _deploy_target + + - name: Deploy docs + shell: bash + run: | + python3 "${GITHUB_WORKSPACE}/.github/scripts/deploy_docs.py" \ + --source-dir ~/_docs_build \ + --target-dir _deploy_target \ + --branch "${{ inputs.source-branch }}" \ + --target-branch "${{ inputs.target-branch }}" \ + --commit-message "${{ inputs.commit-message }}" diff --git a/.github/actions/detect-changes/action.yml b/.github/actions/detect-changes/action.yml index 06b035da4af9..a88ba8788d47 100644 --- a/.github/actions/detect-changes/action.yml +++ b/.github/actions/detect-changes/action.yml @@ -24,115 +24,4 @@ runs: PUSH_BEFORE_SHA: ${{ github.event.before || '' }} MERGE_BASE_SHA: ${{ github.event.merge_group.base_sha || '' }} CURRENT_SHA: ${{ github.sha }} - INPUT_PLATFORM: ${{ inputs.platform }} - run: | - set -euo pipefail - - platform="${INPUT_PLATFORM}" - changed="" - - # Ensure a specific commit object exists locally. With shallow checkouts - # we fetch only the SHAs needed for diff calculation. - ensure_commit() { - local sha="$1" - [[ -z "${sha}" ]] && return 0 - if ! git cat-file -e "${sha}^{commit}" 2>/dev/null; then - git fetch --no-tags --depth=1 origin "${sha}" >/dev/null 2>&1 || true - fi - git cat-file -e "${sha}^{commit}" 2>/dev/null - } - - force_all_true() { - echo "::warning::Unable to compute changed files reliably; forcing build for safety." - echo "any=true" >> "${GITHUB_OUTPUT}" - exit 0 - } - - if [[ "${EVENT_NAME}" == "pull_request" ]]; then - base_sha="${PR_BASE_SHA}" - head_sha="${PR_HEAD_SHA}" - - ensure_commit "${base_sha}" || force_all_true - ensure_commit "${head_sha}" || force_all_true - - changed="$(git diff --name-only "${base_sha}" "${head_sha}" 2>/dev/null)" || force_all_true - elif [[ "${EVENT_NAME}" == "push" ]]; then - before_sha="${PUSH_BEFORE_SHA}" - if [[ -n "${before_sha}" && "${before_sha}" != "0000000000000000000000000000000000000000" ]]; then - ensure_commit "${before_sha}" || force_all_true - ensure_commit "${CURRENT_SHA}" || force_all_true - changed="$(git diff --name-only "${before_sha}" "${CURRENT_SHA}" 2>/dev/null)" || force_all_true - else - changed="$(git diff-tree --no-commit-id --name-only -r "${CURRENT_SHA}")" - fi - elif [[ "${EVENT_NAME}" == "merge_group" && -n "${MERGE_BASE_SHA}" ]]; then - ensure_commit "${MERGE_BASE_SHA}" || force_all_true - ensure_commit "${CURRENT_SHA}" || force_all_true - changed="$(git diff --name-only "${MERGE_BASE_SHA}" "${CURRENT_SHA}" 2>/dev/null)" || force_all_true - else - changed="$(git diff-tree --no-commit-id --name-only -r "${CURRENT_SHA}")" - fi - - echo "Changed files:" - echo "${changed}" - - # Map platform to its workflow file name - case "${platform}" in - docker-*) workflow_name="docker" ;; - *) workflow_name="${platform}" ;; - esac - - relevant_patterns=( - "^src/" - "^CMakeLists.txt$" - "^cmake/" - "^libs/" - "^resources/" - "^translations/" - "^.*\\.qrc$" - "^deploy/${platform}/" - "^test/" - "^\\.github/workflows/${workflow_name}\\.yml$" - "^\\.github/actions/" - "^\\.github/scripts/" - "^\\.github/build-config\\.json$" - ) - - case "${platform}" in - android) relevant_patterns+=("^android/") ;; - docker-linux) - relevant_patterns+=("^deploy/docker/Dockerfile-build-ubuntu$" "^deploy/linux/") - ;; - docker-android) - relevant_patterns+=("^deploy/docker/Dockerfile-build-android$" "^android/" "^deploy/android/") - ;; - esac - - case "${platform}" in - linux) relevant_patterns+=("^tools/setup/.*debian" "^tools/setup/install_dependencies\\.py$") ;; - windows) relevant_patterns+=("^tools/setup/.*windows") ;; - macos) relevant_patterns+=("^tools/setup/.*macos") ;; - android) relevant_patterns+=("^tools/setup/") ;; - ios) relevant_patterns+=("^tools/setup/.*ios" "^tools/setup/.*macos") ;; - docker-linux) relevant_patterns+=("^tools/setup/") ;; - docker-android) relevant_patterns+=("^tools/setup/") ;; - esac - - any_changed="false" - - while IFS= read -r file; do - [[ -z "${file}" ]] && continue - - for pattern in "${relevant_patterns[@]}"; do - if [[ "${file}" =~ ${pattern} ]]; then - any_changed="true" - break - fi - done - - [[ "${any_changed}" == "true" ]] && break - done <<< "${changed}" - - echo "any=${any_changed}" >> "${GITHUB_OUTPUT}" - - echo "Results: any=${any_changed}" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/detect_changes.py" --platform "${{ inputs.platform }}" diff --git a/.github/actions/docker/action.yml b/.github/actions/docker/action.yml index 85afb4b3bdb0..17fdb7ebbda5 100644 --- a/.github/actions/docker/action.yml +++ b/.github/actions/docker/action.yml @@ -41,33 +41,10 @@ runs: - name: Validate inputs shell: bash - env: - DOCKERFILE: ${{ inputs.dockerfile }} - BUILD_TYPE: ${{ inputs.build-type }} run: | - # Validate dockerfile - case "$DOCKERFILE" in - Dockerfile-build-ubuntu|Dockerfile-build-android) - echo "Using dockerfile: $DOCKERFILE" - ;; - *) - echo "Error: Invalid dockerfile '$DOCKERFILE'" >&2 - echo "Allowed values: Dockerfile-build-ubuntu, Dockerfile-build-android" >&2 - exit 1 - ;; - esac - - # Validate build-type - case "$BUILD_TYPE" in - Release|Debug) - echo "Build type: $BUILD_TYPE" - ;; - *) - echo "Error: Invalid build-type '$BUILD_TYPE'" >&2 - echo "Allowed values: Release, Debug" >&2 - exit 1 - ;; - esac + python3 "${GITHUB_WORKSPACE}/.github/scripts/docker_helper.py" validate \ + --dockerfile "${{ inputs.dockerfile }}" \ + --build-type "${{ inputs.build-type }}" - name: Build Docker image if: contains(fromJSON('["Dockerfile-build-ubuntu", "Dockerfile-build-android"]'), inputs.dockerfile) @@ -84,24 +61,9 @@ runs: if: contains(fromJSON('["Release", "Debug"]'), inputs.build-type) shell: bash env: - SOURCE_DIR: ${{ github.workspace }} - BUILD_DIR: ${{ github.workspace }}/build - BUILD_TYPE: ${{ inputs.build-type }} ENABLE_FUSE: ${{ inputs.fuse }} run: | chmod +x ./deploy/docker/docker-run.sh - FUSE_FLAG="" - if [[ "$ENABLE_FUSE" == "true" ]]; then - FUSE_FLAG="--fuse" - fi - max_attempts=2 - attempt=1 - until ./deploy/docker/docker-run.sh ${FUSE_FLAG:+"$FUSE_FLAG"} qgc-builder:latest "$BUILD_TYPE"; do - if (( attempt >= max_attempts )); then - echo "Docker build failed after ${attempt} attempt(s)." >&2 - exit 1 - fi - echo "Docker build failed on attempt ${attempt}. Retrying in 30 seconds..." - sleep 30 - attempt=$((attempt + 1)) - done + ARGS=(run --build-type "${{ inputs.build-type }}") + [[ "$ENABLE_FUSE" == "true" ]] && ARGS+=(--fuse) + python3 "${GITHUB_WORKSPACE}/.github/scripts/docker_helper.py" "${ARGS[@]}" diff --git a/.github/actions/download-all-artifacts/action.yml b/.github/actions/download-all-artifacts/action.yml index dd120e31ac23..49303008326e 100644 --- a/.github/actions/download-all-artifacts/action.yml +++ b/.github/actions/download-all-artifacts/action.yml @@ -51,17 +51,24 @@ runs: INPUT_ARTIFACT_PREFIXES: ${{ inputs.artifact-prefixes }} INPUT_ARTIFACT_METADATA_FILE: ${{ inputs.artifact-metadata-file }} run: | - ARGS=(--repo "$INPUT_REPO" --head-sha "$INPUT_HEAD_SHA" --output-dir "$INPUT_OUTPUT_DIR" --workflows "$INPUT_WORKFLOWS") - if [[ -n "$INPUT_EVENT" ]]; then - ARGS+=(--event "$INPUT_EVENT") - fi - if [[ -n "$RUNS_FILE" && -f "$RUNS_FILE" ]]; then - ARGS+=(--runs-file "$RUNS_FILE") - fi - if [[ -n "$INPUT_ARTIFACT_PREFIXES" ]]; then - ARGS+=(--artifact-prefixes "$INPUT_ARTIFACT_PREFIXES") - fi - if [[ -n "$INPUT_ARTIFACT_METADATA_FILE" ]]; then - ARGS+=(--artifact-metadata-out "$INPUT_ARTIFACT_METADATA_FILE") - fi - python3 "${GITHUB_WORKSPACE}/tools/setup/download_artifacts.py" "${ARGS[@]}" + python3 -c " + import os, subprocess, sys + args = ['${GITHUB_WORKSPACE}/.github/scripts/download_artifacts.py', + '--repo', os.environ['INPUT_REPO'], + '--head-sha', os.environ['INPUT_HEAD_SHA'], + '--output-dir', os.environ['INPUT_OUTPUT_DIR'], + '--workflows', os.environ['INPUT_WORKFLOWS']] + event = os.environ.get('INPUT_EVENT', '') + if event: + args += ['--event', event] + runs = os.environ.get('RUNS_FILE', '') + if runs and os.path.isfile(runs): + args += ['--runs-file', runs] + prefixes = os.environ.get('INPUT_ARTIFACT_PREFIXES', '') + if prefixes: + args += ['--artifact-prefixes', prefixes] + meta = os.environ.get('INPUT_ARTIFACT_METADATA_FILE', '') + if meta: + args += ['--artifact-metadata-out', meta] + sys.exit(subprocess.run([sys.executable] + args).returncode) + " diff --git a/.github/actions/gstreamer/action.yml b/.github/actions/gstreamer/action.yml index 7e4e95b9553f..cb4eaba5a23d 100644 --- a/.github/actions/gstreamer/action.yml +++ b/.github/actions/gstreamer/action.yml @@ -24,11 +24,14 @@ inputs: description: 'Upload to S3' required: false default: 'true' + aws-role-arn: + description: 'AWS IAM role ARN for OIDC authentication (preferred over static credentials)' + required: false aws-access-key-id: - description: 'AWS access key ID' + description: 'AWS access key ID (fallback if OIDC not available)' required: false aws-secret-access-key: - description: 'AWS secret access key' + description: 'AWS secret access key (fallback if OIDC not available)' required: false outputs: @@ -42,36 +45,7 @@ runs: - name: Read build config id: config shell: bash - env: - INPUT_VERSION: ${{ inputs.version }} - INPUT_PLATFORM: ${{ inputs.platform }} - INPUT_ARCH: ${{ inputs.arch }} - run: | - set -euo pipefail - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - echo "py_cmd=$py_cmd" >> "$GITHUB_OUTPUT" - - version="${INPUT_VERSION}" - if [[ -z "$version" ]]; then - case "${INPUT_PLATFORM}" in - macos) version_key="gstreamer_macos_version" ;; - windows) version_key="gstreamer_windows_version" ;; - android) version_key="gstreamer_android_version" ;; - ios) version_key="gstreamer_ios_version" ;; - *) version_key="gstreamer_default_version" ;; - esac - version=$("${py_cmd}" ./tools/setup/read_config.py --get "${version_key}") - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "qt_version=$("${py_cmd}" ./tools/setup/read_config.py --get qt_version)" >> "$GITHUB_OUTPUT" - echo "Building GStreamer $version for ${INPUT_PLATFORM} (${INPUT_ARCH})" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/resolve_gstreamer_config.py" --platform "${{ inputs.platform }}" --version "${{ inputs.version }}" # ===== DEPENDENCIES ===== @@ -283,9 +257,27 @@ runs: Get-ChildItem (Join-Path $prefix 'bin') -Filter '*.exe' | Select-Object Name, Length | Format-Table $libCount = (Get-ChildItem (Join-Path $prefix 'lib') -Filter '*.lib' | Measure-Object).Count Write-Host "$libCount library files found" + $env:PATH = (Join-Path $prefix 'bin') + ";" + $env:PATH + $env:GST_PLUGIN_PATH = Join-Path $prefix 'lib' 'gstreamer-1.0' + & (Join-Path $prefix 'bin' 'gst-launch-1.0.exe') --version # ===== ARCHIVE & UPLOAD ===== + - name: Configure AWS Credentials (OIDC) + if: inputs.upload-s3 == 'true' && inputs.aws-role-arn != '' + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ inputs.aws-role-arn }} + aws-region: us-west-2 + + - name: Configure AWS Credentials (Static) + if: inputs.upload-s3 == 'true' && inputs.aws-role-arn == '' && inputs.aws-access-key-id != '' + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-access-key-id: ${{ inputs.aws-access-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-region: us-west-2 + - name: Create archive id: archive shell: bash @@ -298,8 +290,6 @@ runs: SIMULATOR: ${{ inputs.simulator }} UPLOAD_S3: ${{ inputs.upload-s3 }} WORKSPACE: ${{ github.workspace }} - AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }} - AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }} AWS_DEFAULT_REGION: us-west-2 run: | set -euo pipefail @@ -310,12 +300,12 @@ runs: --output-dir "$OUTPUT_DIR" ) [[ "$SIMULATOR" == "true" ]] && args+=(--simulator) - [[ "$UPLOAD_S3" == "true" && -n "$AWS_ACCESS_KEY_ID" ]] && args+=(--upload-s3) + [[ "$UPLOAD_S3" == "true" ]] && args+=(--upload-s3) "${PY_CMD}" "${WORKSPACE}/.github/scripts/gstreamer_archive.py" "${args[@]}" - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ steps.archive.outputs.name }} path: ${{ steps.archive.outputs.path }} diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index 76f73025792b..b50415b04bbd 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -55,12 +55,7 @@ runs: - name: Enable universe repository (Linux) if: runner.os == 'Linux' shell: bash - run: | - if ! grep -rEq --include='*.list' --include='*.sources' '^(deb|Components:).*universe' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null; then - sudo apt-get -o DPkg::Lock::Timeout=300 -o Acquire::Retries=3 install -y -qq software-properties-common - sudo add-apt-repository -y universe - fi - sudo apt-get -o DPkg::Lock::Timeout=300 -o Acquire::Retries=3 update -y -qq + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" enable-universe - name: Cache and install apt packages (Linux) if: runner.os == 'Linux' @@ -72,62 +67,18 @@ runs: - name: Fix apt alternatives after cache restore (Linux) if: runner.os == 'Linux' shell: bash - run: | - # cache-apt-pkgs-action restores .deb tarballs but may not re-run - # postinst scripts that register update-alternatives entries. - # This leaves symlinks like libblas.so.3 missing even though the - # backing library (e.g. libopenblas) is installed. - if ! sudo dpkg --configure -a; then - echo "::warning::dpkg --configure -a failed; package state may be inconsistent" - fi - - multiarch="$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null || true)" - blas_link="" - if [[ -n "${multiarch}" ]]; then - blas_link="/usr/lib/${multiarch}/libblas.so.3" - fi - - # Ensure BLAS SONAME is resolvable before AppImage dependency bundling. - if ! ldconfig -p | grep -qE '\blibblas\.so\.3\b'; then - echo "::warning::libblas.so.3 missing from linker cache; attempting repair" - sudo apt-get -o DPkg::Lock::Timeout=300 -o Acquire::Retries=3 install -y -qq --reinstall libblas3 libopenblas0 || true - if [[ -n "${multiarch}" ]]; then - sudo update-alternatives --auto "libblas.so.3-${multiarch}" || true - fi - - if [[ -n "${blas_link}" && ! -e "${blas_link}" ]]; then - candidate="$(find /usr/lib -type f -name 'libblas.so.3' 2>/dev/null | head -n1 || true)" - if [[ -n "${candidate}" ]]; then - echo "::warning::Creating compatibility symlink ${blas_link} -> ${candidate}" - sudo ln -sf "${candidate}" "${blas_link}" - fi - fi - sudo ldconfig - fi - - if ! ldconfig -p | grep -qE '\blibblas\.so\.3\b'; then - echo "::error::libblas.so.3 is still missing after repair attempt" - exit 1 - fi + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" fix-apt-alternatives - name: Install optional apt packages (Linux) if: runner.os == 'Linux' shell: bash - run: | - optional=$(python3 tools/setup/install_dependencies.py --platform debian --category gstreamer_optional --print-available-packages) - if [ -n "$optional" ]; then - echo "Installing optional packages: $optional" - # shellcheck disable=SC2086 - sudo apt-get -o DPkg::Lock::Timeout=300 install -y -qq $optional || true - fi + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" install-optional - name: Detect Python version for pipx cache (Linux) if: runner.os == 'Linux' id: python-version shell: bash - run: | - py_minor="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" - echo "minor=${py_minor}" >> "$GITHUB_OUTPUT" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" detect-python-version - name: Cache pipx and pip downloads (Linux) if: runner.os == 'Linux' diff --git a/.github/actions/qt-android/action.yml b/.github/actions/qt-android/action.yml index a310d6206da5..04ca94010431 100644 --- a/.github/actions/qt-android/action.yml +++ b/.github/actions/qt-android/action.yml @@ -1,5 +1,12 @@ name: Android Qt description: Install Qt for Android +outputs: + host_qt_root_dir: + description: Desktop Qt root used for host tools + value: ${{ steps.qt-host.outputs.qt_root_dir }} + target_qt_root_dir: + description: Android Qt root used for target packages + value: ${{ steps.qt-target.outputs.qt_root_dir }} inputs: host: description: Host @@ -45,9 +52,9 @@ inputs: required: false default: '35.0.0' save-cache: - description: Save cache (set to false for PR builds) + description: "Whether to save cache (auto = save for pushes and same-repo PRs, skip for fork PRs)" required: false - default: 'true' + default: 'auto' runs: using: composite steps: @@ -71,22 +78,9 @@ runs: - name: Update Android SDK / NDK / Tools shell: bash run: | - ANDROID_SDK_ROOT_UNIX="${ANDROID_SDK_ROOT//\\//}" - NDK_PATH="${ANDROID_SDK_ROOT_UNIX}/ndk/${{ inputs.ndk-full-version }}" - if [[ ! -d "$NDK_PATH" ]]; then - echo "::error::NDK path not found: $NDK_PATH" - exit 1 - fi - echo "ANDROID_NDK_ROOT=${NDK_PATH}" >> "$GITHUB_ENV" - echo "ANDROID_NDK_HOME=${NDK_PATH}" >> "$GITHUB_ENV" - echo "ANDROID_NDK=${NDK_PATH}" >> "$GITHUB_ENV" - if [[ "$RUNNER_OS" == "Windows" ]]; then - "${ANDROID_SDK_ROOT}"/cmdline-tools/latest/bin/sdkmanager.bat --update - "${GITHUB_WORKSPACE}"/android/gradlew.bat --version - else - sdkmanager --update - "${GITHUB_WORKSPACE}"/android/gradlew --version - fi + python3 "${GITHUB_WORKSPACE}/.github/scripts/android_sdk_helper.py" \ + --ndk-version "${{ inputs.ndk-full-version }}" \ + --workspace "${GITHUB_WORKSPACE}" - name: Setup Caching uses: ./.github/actions/cache @@ -98,7 +92,8 @@ runs: save-cache: ${{ inputs.save-cache }} - name: Install Qt for ${{ runner.os }} - uses: jurplel/install-qt-action@v4 + id: qt-host + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -106,12 +101,11 @@ runs: arch: ${{ inputs.arch }} dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true - name: Install Qt for Android (arm64_v8a) if: contains(format(';{0};', inputs.abis), ';arm64-v8a;') - uses: jurplel/install-qt-action@v4 + id: qt-android-arm64 + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -119,12 +113,12 @@ runs: arch: android_arm64_v8a dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true + export-env: 'false' - name: Install Qt for Android (armv7) if: contains(format(';{0};', inputs.abis), ';armeabi-v7a;') - uses: jurplel/install-qt-action@v4 + id: qt-android-armv7 + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -132,12 +126,12 @@ runs: arch: android_armv7 dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true + export-env: 'false' - name: Install Qt for Android (x86_64) if: contains(format(';{0};', inputs.abis), ';x86_64;') - uses: jurplel/install-qt-action@v4 + id: qt-android-x86_64 + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -145,12 +139,12 @@ runs: arch: android_x86_64 dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true + export-env: 'false' - name: Install Qt for Android (x86) if: contains(format(';{0};', inputs.abis), ';x86;') - uses: jurplel/install-qt-action@v4 + id: qt-android-x86 + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -158,5 +152,15 @@ runs: arch: android_x86 dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true + export-env: 'false' + + - name: Resolve primary Android Qt root + id: qt-target + shell: bash + run: | + python3 "${GITHUB_WORKSPACE}/tools/setup/install_qt.py" resolve-android-root \ + --abis "${{ inputs.abis }}" \ + --arm64 "${{ steps.qt-android-arm64.outputs.qt_root_dir }}" \ + --armv7 "${{ steps.qt-android-armv7.outputs.qt_root_dir }}" \ + --x86-64 "${{ steps.qt-android-x86_64.outputs.qt_root_dir }}" \ + --x86 "${{ steps.qt-android-x86.outputs.qt_root_dir }}" diff --git a/.github/actions/qt-install/action.yml b/.github/actions/qt-install/action.yml index 84ebea58f3e4..853cc08b29c4 100644 --- a/.github/actions/qt-install/action.yml +++ b/.github/actions/qt-install/action.yml @@ -1,5 +1,15 @@ name: Install Qt -description: Install Qt with standard QGroundControl modules +description: Install Qt using aqtinstall with caching +outputs: + arch_dir: + description: Resolved Qt architecture directory name + value: ${{ steps.qt-meta.outputs.arch_dir }} + qt_root_dir: + description: Absolute Qt root directory + value: ${{ steps.qt-install.outputs.qt_root_dir || steps.qt-cached.outputs.qt_root_dir }} + qt_bin_dir: + description: Qt bin directory + value: ${{ steps.qt-install.outputs.qt_bin_dir || steps.qt-cached.outputs.qt_bin_dir }} inputs: version: description: Qt version to install @@ -26,21 +36,69 @@ inputs: description: Optional Qt archives subset to install (space-separated) required: false default: '' + export-env: + description: Export QT_ROOT_DIR and prepend the Qt bin directory to PATH + required: false + default: 'true' runs: using: composite steps: - - name: Ensure pip available + - name: Resolve arch directory and cache key + id: qt-meta shell: bash - run: python3 -m ensurepip 2>/dev/null || true - - name: Install Qt ${{ inputs.version }} - uses: jurplel/install-qt-action@v4 + env: + QT_ARCH: ${{ inputs.arch }} + QT_MODULES: ${{ inputs.modules }} + QT_ARCHIVES: ${{ inputs.archives }} + run: python3 "${GITHUB_WORKSPACE}/tools/setup/install_qt.py" cache-key --arch "${QT_ARCH}" --modules "${QT_MODULES}" --archives "${QT_ARCHIVES}" + + - name: Restore extracted Qt cache + id: qt-cache + uses: actions/cache@v5 with: - version: ${{ inputs.version }} - host: ${{ inputs.host }} - target: ${{ inputs.target }} - arch: ${{ inputs.arch }} - dir: ${{ inputs.dir }} - modules: ${{ inputs.modules }} - archives: ${{ inputs.archives }} - setup-python: false - cache: true + path: ${{ inputs.dir }}/Qt/${{ inputs.version }} + key: qt-${{ runner.os }}-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.arch }}-${{ inputs.version }}-${{ steps.qt-meta.outputs.digest }} + + - name: Install Qt ${{ inputs.version }} + if: steps.qt-cache.outputs.cache-hit != 'true' + id: qt-install + shell: bash + env: + QT_VERSION: ${{ inputs.version }} + QT_HOST: ${{ inputs.host }} + QT_TARGET: ${{ inputs.target }} + QT_ARCH: ${{ inputs.arch }} + QT_OUTDIR: ${{ inputs.dir }}/Qt + QT_MODULES: ${{ inputs.modules }} + QT_ARCHIVES: ${{ inputs.archives }} + run: | + python3 "${GITHUB_WORKSPACE}/tools/setup/install_qt.py" install \ + --version "${QT_VERSION}" \ + --host "${QT_HOST}" \ + --target "${QT_TARGET}" \ + --arch "${QT_ARCH}" \ + --outdir "${QT_OUTDIR}" \ + --modules "${QT_MODULES}" \ + --archives "${QT_ARCHIVES}" + + - name: Resolve Qt paths (cache hit) + if: steps.qt-cache.outputs.cache-hit == 'true' + id: qt-cached + shell: bash + env: + QT_OUTDIR: ${{ inputs.dir }}/Qt + QT_VERSION: ${{ inputs.version }} + ARCH_DIR: ${{ steps.qt-meta.outputs.arch_dir }} + run: | + qt_root="${QT_OUTDIR}/${QT_VERSION}/${ARCH_DIR}" + echo "qt_root_dir=${qt_root}" >> "$GITHUB_OUTPUT" + echo "qt_bin_dir=${qt_root}/bin" >> "$GITHUB_OUTPUT" + + - name: Set Qt environment + if: ${{ inputs.export-env == 'true' }} + shell: bash + run: | + qt_root="${{ steps.qt-install.outputs.qt_root_dir || steps.qt-cached.outputs.qt_root_dir }}" + echo "QT_ROOT_DIR=${qt_root}" >> "$GITHUB_ENV" + echo "${qt_root}/bin" >> "$GITHUB_PATH" + echo "Qt installed at ${qt_root}" diff --git a/.github/actions/qt-ios/action.yml b/.github/actions/qt-ios/action.yml index 05ca7b01315d..072210416da3 100644 --- a/.github/actions/qt-ios/action.yml +++ b/.github/actions/qt-ios/action.yml @@ -24,9 +24,9 @@ inputs: required: false default: Release save-cache: - description: Save cache (set to false for PR builds) + description: "Whether to save cache (auto = save for pushes and same-repo PRs, skip for fork PRs)" required: false - default: "true" + default: "auto" restore-timestamps: description: Restore file timestamps from git history (requires fetch-depth 0) required: false diff --git a/.github/actions/run-unit-tests/action.yml b/.github/actions/run-unit-tests/action.yml index 9db5a51bc936..eb56af76a104 100644 --- a/.github/actions/run-unit-tests/action.yml +++ b/.github/actions/run-unit-tests/action.yml @@ -32,52 +32,17 @@ runs: - name: Detect parallel jobs id: jobs shell: bash - env: - INPUT_PARALLEL: ${{ inputs.parallel }} run: | - if [[ "$INPUT_PARALLEL" != "auto" ]]; then - JOBS="$INPUT_PARALLEL" - elif command -v nproc >/dev/null 2>&1; then - JOBS="$(nproc)" - elif command -v sysctl >/dev/null 2>&1; then - JOBS="$(sysctl -n hw.ncpu)" - elif [[ -n "${NUMBER_OF_PROCESSORS:-}" ]]; then - JOBS="${NUMBER_OF_PROCESSORS}" - else - JOBS=2 - fi - - if [[ ! "$JOBS" =~ ^[0-9]+$ ]] || [[ "$JOBS" -lt 1 ]]; then - echo "::error::Invalid parallel job count: $JOBS" - exit 1 - fi - - echo "jobs=$JOBS" >> "$GITHUB_OUTPUT" + python3 "${GITHUB_WORKSPACE}/.github/scripts/cmake_helper.py" detect-jobs \ + --parallel "${{ inputs.parallel }}" - name: Run CTest shell: bash working-directory: ${{ inputs.build-dir }} - env: - INPUT_JUNIT_OUTPUT: ${{ inputs.junit-output }} - INPUT_CTEST_OUTPUT: ${{ inputs.ctest-output }} - INPUT_INCLUDE_LABELS: ${{ inputs.include-labels }} - INPUT_EXCLUDE_LABELS: ${{ inputs.exclude-labels }} - INPUT_JOBS: ${{ steps.jobs.outputs.jobs }} run: | - set -o pipefail - - CTEST_ARGS=( - --output-on-failure - --output-junit "$INPUT_JUNIT_OUTPUT" - --parallel "$INPUT_JOBS" - ) - - if [[ -n "$INPUT_INCLUDE_LABELS" ]]; then - CTEST_ARGS+=(-L "$INPUT_INCLUDE_LABELS") - fi - - if [[ -n "$INPUT_EXCLUDE_LABELS" ]]; then - CTEST_ARGS+=(-LE "$INPUT_EXCLUDE_LABELS") - fi - - ctest "${CTEST_ARGS[@]}" 2>&1 | tee "$INPUT_CTEST_OUTPUT" + python3 "${GITHUB_WORKSPACE}/.github/scripts/cmake_helper.py" ctest \ + --junit-output "${{ inputs.junit-output }}" \ + --ctest-output "${{ inputs.ctest-output }}" \ + --jobs "${{ steps.jobs.outputs.jobs }}" \ + --include-labels "${{ inputs.include-labels }}" \ + --exclude-labels "${{ inputs.exclude-labels }}" diff --git a/.github/actions/upload/action.yml b/.github/actions/upload/action.yml index 0d8102c48165..1436575f9982 100644 --- a/.github/actions/upload/action.yml +++ b/.github/actions/upload/action.yml @@ -2,33 +2,33 @@ name: Upload Release description: Upload release artifact to GitHub and optionally to AWS inputs: - artifact_name: + artifact-name: description: Artifact filename required: true - package_name: + package-name: description: Package name (for GitHub artifact) required: true - aws_role_arn: + aws-role-arn: description: AWS IAM role ARN for OIDC authentication (preferred) required: false - aws_key_id: + aws-key-id: description: AWS access key ID required: false - aws_secret_access_key: + aws-secret-access-key: description: AWS secret access key required: false - aws_distribution_id: + aws-distribution-id: description: AWS CloudFront distribution ID required: false - upload_aws: + upload-aws: description: Upload artifact to AWS required: false default: 'true' - retention_days: + retention-days: description: Days to retain artifact (default 7 for PRs, 30 for pushes) required: false default: '' - compression_level: + compression-level: description: Compression level 0-9 (0=none, best for pre-compressed binaries) required: false default: '0' @@ -37,24 +37,24 @@ runs: using: composite steps: - name: Save artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: - name: ${{ inputs.package_name }} - path: ${{ runner.temp }}/build/${{ inputs.artifact_name }} - retention-days: ${{ inputs.retention_days != '' && inputs.retention_days || (github.event_name == 'pull_request' && '7' || '30') }} - compression-level: ${{ inputs.compression_level }} + name: ${{ inputs.package-name }} + path: ${{ runner.temp }}/build/${{ inputs.artifact-name }} + retention-days: ${{ inputs.retention-days != '' && inputs.retention-days || (github.event_name == 'pull_request' && '7' || '30') }} + compression-level: ${{ inputs.compression-level }} - name: Upload to AWS if: >- - fromJSON(inputs.upload_aws) && + fromJSON(inputs.upload-aws) && github.event_name == 'push' && github.repository_owner == 'mavlink' && - (inputs.aws_role_arn != '' || (inputs.aws_key_id != '' && inputs.aws_secret_access_key != '')) + (inputs.aws-role-arn != '' || (inputs.aws-key-id != '' && inputs.aws-secret-access-key != '')) uses: ./.github/actions/aws-upload with: - artifact_name: ${{ inputs.artifact_name }} - artifact_path: ${{ runner.temp }}/build/${{ inputs.artifact_name }} - aws_role_arn: ${{ inputs.aws_role_arn }} - aws_key_id: ${{ inputs.aws_key_id }} - aws_secret_access_key: ${{ inputs.aws_secret_access_key }} - aws_distribution_id: ${{ inputs.aws_distribution_id }} + artifact-name: ${{ inputs.artifact-name }} + artifact-path: ${{ runner.temp }}/build/${{ inputs.artifact-name }} + aws-role-arn: ${{ inputs.aws-role-arn }} + aws-key-id: ${{ inputs.aws-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-distribution-id: ${{ inputs.aws-distribution-id }} diff --git a/.github/actions/verify-executable/action.yml b/.github/actions/verify-executable/action.yml index dfeb553b82b1..65025b1ad8a4 100644 --- a/.github/actions/verify-executable/action.yml +++ b/.github/actions/verify-executable/action.yml @@ -23,58 +23,9 @@ runs: steps: - name: Verify executable shell: bash - env: - INPUT_BINARY_PATH: ${{ inputs.binary-path }} - INPUT_WORKING_DIR: ${{ inputs.working-dir }} - INPUT_TYPE: ${{ inputs.type }} - INPUT_TIMEOUT: ${{ inputs.timeout }} run: | - set -euo pipefail - - binary_path="${INPUT_BINARY_PATH}" - - if [[ -n "${INPUT_WORKING_DIR}" ]]; then - work_dir="${INPUT_WORKING_DIR}" - else - work_dir="$(dirname "${binary_path}")" - fi - - binary_name="$(basename "${binary_path}")" - run_binary="${binary_name}" - run_headless="true" - - # AppImages typically don't ship the "offscreen" Qt platform plugin. - # Let run_tests.py use xvfb-run (Linux, no DISPLAY) instead. - if [[ "${INPUT_TYPE}" == "appimage" || "${binary_name}" == *.AppImage ]]; then - run_headless="false" - fi - - if [[ ! -d "${work_dir}" ]]; then - echo "::error::Working directory not found: ${work_dir}" - exit 1 - fi - - pushd "${work_dir}" >/dev/null - - if [[ "${RUNNER_OS}" != "Windows" ]]; then - chmod +x "${binary_name}" 2>/dev/null || true - run_binary="${PWD}/${binary_name}" - fi - - set +e - args=( - --binary "${run_binary}" - --timeout "${INPUT_TIMEOUT}" - --verify-only - -v - ) - if [[ "${run_headless}" == "true" ]]; then - args+=(--headless) - fi - python3 "${GITHUB_WORKSPACE}/tools/run_tests.py" "${args[@]}" - exit_code=$? - set -e - - popd >/dev/null - - exit "${exit_code}" + python3 "${GITHUB_WORKSPACE}/.github/scripts/verify_executable.py" \ + --binary-path "${{ inputs.binary-path }}" \ + --type "${{ inputs.type }}" \ + --working-dir "${{ inputs.working-dir }}" \ + --timeout "${{ inputs.timeout }}" diff --git a/.github/build-config.json b/.github/build-config.json index e87c5c7a254b..599ce585893b 100644 --- a/.github/build-config.json +++ b/.github/build-config.json @@ -15,6 +15,7 @@ "java_version": "17", "macos_deployment_target": "13.0", "ndk_full_version": "27.2.12479018", + "platform_workflows": "Linux,Windows,MacOS,Android", "ndk_version": "r27c", "qt_minimum_version": "6.10.0", "qt_modules": "qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml qtwebsockets qthttpserver", diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c31297820aa9..d9963c4a1597 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -39,6 +39,42 @@ src/ └── Settings/ # Persistent settings ``` +## CI Structure + +Platform workflows (`linux.yml`, `macos.yml`, `windows.yml`, `android.yml`, `ios.yml`) share logic via composite actions and reusable workflows. + +``` +.github/ +├── workflows/ +│ ├── linux.yml, macos.yml, ... # Platform build + test +│ ├── _detect-changes.yml # Reusable: skip builds on unrelated PRs +│ ├── build-results.yml # Aggregate PR comment (workflow_run trigger) +│ └── build-gstreamer.yml # GStreamer SDK builds +├── actions/ +│ ├── cmake-configure/ # CMake configure with consistent options +│ ├── cmake-build/ # Build with timing, reviewdog, ccache +│ ├── run-unit-tests/ # CTest runner with JUnit output +│ ├── detect-changes/ # Path-based change detection per platform +│ ├── attest-and-upload/ # SBOM attestation + artifact upload +│ ├── deploy-docs/ # Deploy built docs to external repo +│ ├── gstreamer/ # Build GStreamer from source +│ ├── setup-python/ # Python + uv + dependency installation +│ └── qt-install/ # Qt SDK installation with caching +├── scripts/ # Python scripts for CI jobs +│ ├── common/ # Shared modules (gh_actions, build_config) +│ ├── templates/ # Jinja2 templates for generated output +│ └── tests/ # Tests for CI scripts +└── build-config.json # Centralized version numbers +``` + +### CI Conventions + +- **Dependencies**: CI Python scripts use `httpx` for GitHub API access and `jinja2` for templating. Deps managed in `tools/pyproject.toml` under `[project.optional-dependencies] scripts`. +- **Shared helpers**: `gh_actions.py` provides GitHub API pagination (httpx) with `gh` CLI fallback. Import as `from common.gh_actions import ...`. +- **Bootstrap scripts** (`install_dependencies.py`, `ccache_helper.py`): Use stdlib only — they run before dependencies are installed. +- **Config**: Version numbers and build settings live in `.github/build-config.json`. Read via `common.build_config.get_build_config_value()`. +- **Outputs**: Use `common.gh_actions.write_github_output()` for `$GITHUB_OUTPUT` writes. + ## Quick Patterns ```cpp diff --git a/.github/scripts/android_sdk_helper.py b/.github/scripts/android_sdk_helper.py new file mode 100644 index 000000000000..affcd1a39881 --- /dev/null +++ b/.github/scripts/android_sdk_helper.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Android SDK/NDK setup helpers for CI.""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from pathlib import Path + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import append_github_env # noqa: E402 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--ndk-version", required=True) + parser.add_argument("--workspace", default=os.environ.get("GITHUB_WORKSPACE", ".")) + args = parser.parse_args() + + sdk_root = os.environ.get("ANDROID_SDK_ROOT", "") + if not sdk_root: + print("::error::ANDROID_SDK_ROOT not set", file=sys.stderr) + sys.exit(1) + + sdk_root_unix = sdk_root.replace("\\", "/") + ndk_path = f"{sdk_root_unix}/ndk/{args.ndk_version}" + + if not Path(ndk_path).is_dir(): + print(f"::error::NDK path not found: {ndk_path}", file=sys.stderr) + sys.exit(1) + + append_github_env({ + "ANDROID_NDK_ROOT": ndk_path, + "ANDROID_NDK_HOME": ndk_path, + "ANDROID_NDK": ndk_path, + }) + + is_windows = os.environ.get("RUNNER_OS") == "Windows" + + if is_windows: + sdkmanager = os.path.join(sdk_root, "cmdline-tools", "latest", "bin", "sdkmanager.bat") + gradlew = os.path.join(args.workspace, "android", "gradlew.bat") + else: + sdkmanager = "sdkmanager" + gradlew = os.path.join(args.workspace, "android", "gradlew") + + subprocess.run([sdkmanager, "--update"], check=True) + subprocess.run([gradlew, "--version"], check=True) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/attest_helper.py b/.github/scripts/attest_helper.py new file mode 100644 index 000000000000..f59fdb791c10 --- /dev/null +++ b/.github/scripts/attest_helper.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Check attestation conditions and resolve SBOM paths.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import is_fork_pr, write_github_output # noqa: E402 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--subject-path", required=True) + parser.add_argument("--subject-name", required=True) + parser.add_argument("--scan-path", default="") + parser.add_argument("--sbom-format", default="spdx-json") + parser.add_argument("--runner-temp", required=True) + args = parser.parse_args() + + subject = Path(args.subject_path) + + if is_fork_pr(): + print("Skipping attestation for external PR") + write_github_output({"skip": "true"}) + return + + if not subject.exists(): + print(f"::warning::Artifact not found: {subject}") + write_github_output({"skip": "true"}) + return + + scan_path = args.scan_path or str(subject.parent) + suffix = "cdx.json" if args.sbom_format == "cyclonedx-json" else "spdx.json" + sbom_path = str(Path(args.runner_temp) / f"{args.subject_name}.sbom.{suffix}") + + write_github_output({ + "skip": "false", + "scan-path": scan_path, + "sbom-path": sbom_path, + }) + print(f"Will attest: {subject}") + print(f"Scan path: {scan_path}") + print(f"SBOM path: {sbom_path}") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/aws_upload.py b/.github/scripts/aws_upload.py new file mode 100644 index 000000000000..cc21cf4b2b80 --- /dev/null +++ b/.github/scripts/aws_upload.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Validate and upload artifacts to AWS S3. + +Subcommands: + validate Validate AWS credentials and artifact inputs + upload Upload artifact to S3 with path safety checks +""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from pathlib import Path + + +def validate_credentials(role_arn: str, key_id: str, secret_key: str) -> None: + """Ensure either OIDC role or static credentials are provided.""" + if not role_arn and (not key_id or not secret_key): + print( + "::error::Either aws_role_arn (OIDC) or both aws_key_id and " + "aws_secret_access_key (static credentials) must be provided", + file=sys.stderr, + ) + sys.exit(1) + + +def validate_artifact(artifact_path: str, artifact_name: str) -> None: + """Validate artifact exists and name is safe.""" + if not Path(artifact_path).is_file(): + print(f"::error::Artifact not found: {artifact_path}", file=sys.stderr) + sys.exit(1) + + if re.search(r"[/\\]", artifact_name) or ".." in artifact_name: + print( + f"::error::Invalid artifact name (contains path separators or ..): {artifact_name}", + file=sys.stderr, + ) + sys.exit(1) + + +def sanitize_ref(ref_name: str) -> str: + """Remove path traversal and unsafe characters from a git ref name.""" + safe = ref_name.replace("..", "") + return re.sub(r"[^a-zA-Z0-9._-]", "_", safe) + + +def cmd_validate(args: argparse.Namespace) -> None: + validate_credentials(args.role_arn, args.key_id, args.secret_key) + validate_artifact(args.artifact_path, args.artifact_name) + + +def cmd_upload(args: argparse.Namespace) -> None: + validate_artifact(args.artifact_path, args.artifact_name) + safe_ref = sanitize_ref(args.ref_name) + + s3_path = f"s3://{args.s3_bucket}/builds/{safe_ref}/{args.artifact_name}" + subprocess.run( + ["aws", "s3", "cp", args.artifact_path, s3_path, "--acl", "public-read"], + check=True, + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + p_val = sub.add_parser("validate") + p_val.add_argument("--role-arn", default="") + p_val.add_argument("--key-id", default="") + p_val.add_argument("--secret-key", default="") + p_val.add_argument("--artifact-path", required=True) + p_val.add_argument("--artifact-name", required=True) + + p_up = sub.add_parser("upload") + p_up.add_argument("--artifact-path", required=True) + p_up.add_argument("--artifact-name", required=True) + p_up.add_argument("--ref-name", required=True) + p_up.add_argument("--s3-bucket", default="qgroundcontrol") + + args = parser.parse_args() + {"validate": cmd_validate, "upload": cmd_upload}[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/benchmark_runner.py b/.github/scripts/benchmark_runner.py index 582669840e30..ccfc06184b9b 100755 --- a/.github/scripts/benchmark_runner.py +++ b/.github/scripts/benchmark_runner.py @@ -18,6 +18,15 @@ import time from pathlib import Path +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import ( # noqa: E402 + write_github_output as _write_github_output, + write_step_summary as _write_step_summary, +) + def run_benchmarks(binary: Path, test_filter: str, platform: str) -> str: """Run Qt Test benchmarks and return output.""" @@ -106,33 +115,25 @@ def fallback_startup_benchmark(binary: Path, platform: str) -> list[dict]: def write_github_output(results: list[dict]) -> None: """Write outputs for GitHub Actions.""" - github_output = os.environ.get("GITHUB_OUTPUT") - if github_output: - with open(github_output, "a") as f: - f.write(f"has_results={'true' if results else 'false'}\n") - f.write(f"result_count={len(results)}\n") + _write_github_output({ + "has_results": "true" if results else "false", + "result_count": str(len(results)), + }) def write_summary(results: list[dict], output_file: Path) -> None: """Write GitHub step summary.""" - summary_file = os.environ.get("GITHUB_STEP_SUMMARY") - if not summary_file: - return - - with open(summary_file, "a") as f: - f.write("## Runtime Benchmarks\n\n") - f.write( - "⚠️ **Note**: Runtime benchmarks on shared runners have ~3x variance.\n" - ) - f.write("Only significant regressions (>50%) trigger alerts.\n\n") - f.write("### Results\n\n") - - if results and output_file.exists(): - f.write("```json\n") - f.write(output_file.read_text()) - f.write("\n```\n") - else: - f.write("No benchmark results available\n") + parts = [ + "## Runtime Benchmarks\n", + "⚠️ **Note**: Runtime benchmarks on shared runners have ~3x variance.", + "Only significant regressions (>50%) trigger alerts.\n", + "### Results\n", + ] + if results and output_file.exists(): + parts.append(f"```json\n{output_file.read_text()}\n```\n") + else: + parts.append("No benchmark results available\n") + _write_step_summary("\n".join(parts)) def main() -> int: diff --git a/.github/scripts/ccache_helper.py b/.github/scripts/ccache_helper.py index d2c31733da80..952423fac421 100755 --- a/.github/scripts/ccache_helper.py +++ b/.github/scripts/ccache_helper.py @@ -30,8 +30,25 @@ import zipfile from pathlib import Path from typing import NamedTuple -from urllib.error import HTTPError, URLError -from urllib.request import urlretrieve +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import append_github_env, write_github_output as write_github_outputs # noqa: E402 + +def _download_file(url: str, dest: Path, *, timeout: int = 120) -> None: + """Download a URL to a local file using urllib (stdlib).""" + import urllib.request + + request = urllib.request.Request(url) + with urllib.request.urlopen(request, timeout=timeout) as resp: + with open(dest, "wb") as f: + while True: + chunk = resp.read(65536) + if not chunk: + break + f.write(chunk) + WINDOWS_BINARY_SHA256 = { "x86_64": "1c78a0b816a3174d4b170b96294e016a21fb4a577dfd8361e7322f77f85c6348", @@ -136,12 +153,14 @@ def read_max_size(self, config_path: Path) -> str: def download_with_retry(self, url: str, dest: Path) -> bool: """Download file with retry logic.""" + from urllib.error import URLError + for attempt in range(1, self.max_retries + 1): try: print(f"Downloading {url} (attempt {attempt}/{self.max_retries})") - urlretrieve(url, dest) + _download_file(url, dest) return True - except (HTTPError, URLError, OSError) as e: + except (URLError, OSError) as e: print(f"Download failed: {e}", file=sys.stderr) if attempt < self.max_retries: print(f"Retrying in {self.retry_delay} seconds...") @@ -461,41 +480,12 @@ def run_summary() -> int: def output_github_actions(config: CcacheConfig) -> None: """Write outputs for GitHub Actions.""" - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - - try: - with open(github_output, "a") as f: - f.write(f"version={config.version}\n") - f.write(f"arch={config.arch}\n") - f.write(f"max_size={config.max_size}\n") - except OSError as e: - print(f"Warning: Failed to write GitHub output: {e}", file=sys.stderr) - + write_github_outputs({ + "version": config.version, + "arch": config.arch, + "max_size": config.max_size, + }) -def write_github_outputs(values: dict[str, str]) -> None: - """Write arbitrary key/value pairs to ``$GITHUB_OUTPUT`` when available.""" - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - - try: - with open(github_output, "a") as f: - for key, value in values.items(): - f.write(f"{key}={value}\n") - except OSError as e: - print(f"Warning: Failed to write GitHub output: {e}", file=sys.stderr) - - -def append_github_env(values: dict[str, str]) -> None: - """Append environment variables to ``$GITHUB_ENV`` when available.""" - github_env = os.environ.get("GITHUB_ENV") - if not github_env: - return - with open(github_env, "a", encoding="utf-8") as handle: - for key, value in values.items(): - handle.write(f"{key}={value}\n") def append_github_path(path_entry: str) -> None: @@ -593,7 +583,7 @@ def install_windows_binary(version: str, arch: str, sha256: str, runner_temp: Pa with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) archive_path = temp_path / "ccache.zip" - urlretrieve(ccache_url, archive_path) + _download_file(ccache_url, archive_path) actual = hashlib.sha256(archive_path.read_bytes()).hexdigest() if actual != sha256: raise RuntimeError(f"SHA256 mismatch: {actual} != {sha256}") diff --git a/.github/scripts/check_baseline_ready.py b/.github/scripts/check_baseline_ready.py index 287132478985..5d7c5c34a465 100644 --- a/.github/scripts/check_baseline_ready.py +++ b/.github/scripts/check_baseline_ready.py @@ -4,7 +4,6 @@ from __future__ import annotations import argparse -import hashlib import json import os from typing import Any @@ -13,12 +12,7 @@ ensure_tools_dir(__file__) -from common.gh_actions import list_workflow_runs_for_sha - - -def parse_csv_list(value: str) -> list[str]: - """Parse comma-separated values into a trimmed non-empty list.""" - return [item.strip() for item in value.split(",") if item.strip()] +from common.gh_actions import list_workflow_runs_for_sha, parse_csv_list, write_github_output def evaluate_readiness( @@ -56,21 +50,6 @@ def evaluate_readiness( return ready, missing, incomplete, failed -def write_output(key: str, value: str) -> None: - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - if "\n" in value: - value_hash = hashlib.sha256(value.encode("utf-8")).hexdigest()[:12] - delim = f"EOF_{key}_{value_hash}" - while delim in value: - delim = f"{delim}_X" - with open(github_output, "a", encoding="utf-8") as f: - f.write(f"{key}<<{delim}\n{value}\n{delim}\n") - else: - with open(github_output, "a", encoding="utf-8") as f: - f.write(f"{key}={value}\n") - def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Check baseline readiness for build-results workflow.") @@ -105,10 +84,12 @@ def main(argv: list[str] | None = None) -> int: ready, missing, incomplete, failed = evaluate_readiness(runs, platforms, args.event) - write_output("ready", "true" if ready else "false") - write_output("missing", ",".join(missing)) - write_output("incomplete", ",".join(incomplete)) - write_output("failed", ",".join(failed)) + write_github_output({ + "ready": "true" if ready else "false", + "missing": ",".join(missing), + "incomplete": ",".join(incomplete), + "failed": ",".join(failed), + }) if ready: print(f"Baseline ready for {args.head_sha}.") diff --git a/.github/scripts/cmake_helper.py b/.github/scripts/cmake_helper.py new file mode 100644 index 000000000000..4a27cd0d98b3 --- /dev/null +++ b/.github/scripts/cmake_helper.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""CMake build and configure helpers for CI. + +Subcommands: + build Run cmake --build with CI-friendly output and timing + configure Run configure.py with standardized arguments + detect-jobs Detect number of parallel jobs for the current platform + ctest Run CTest with standardized arguments and timing +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +import time + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import append_github_env, write_github_output # noqa: E402 + + +def detect_jobs(requested: str = "auto") -> int: + """Detect number of parallel jobs for the current platform.""" + if requested != "auto": + if not re.match(r"^[1-9]\d*$", requested): + print(f"::error::Invalid parallel job count: {requested}", file=sys.stderr) + sys.exit(1) + return int(requested) + + try: + return os.cpu_count() or 2 + except Exception: + return 2 + + +def cmd_detect_jobs(args: argparse.Namespace) -> None: + jobs = detect_jobs(args.parallel) + write_github_output({"jobs": str(jobs)}) + + +def cmd_build(args: argparse.Namespace) -> None: + """Run cmake --build with CI-friendly output.""" + cmd = ["cmake", "--build", "."] + + if args.target: + cmd += ["--target", args.target] + if args.build_type: + cmd += ["--config", args.build_type] + if args.parallel: + if args.parallel_jobs: + if not re.match(r"^[1-9]\d*$", args.parallel_jobs): + print(f"::error::parallel-jobs must be a positive integer, got '{args.parallel_jobs}'", + file=sys.stderr) + sys.exit(1) + cmd += ["--parallel", args.parallel_jobs] + else: + cmd.append("--parallel") + + output_file = args.output_file + if args.reviewdog and not output_file: + output_file = os.path.join(os.environ.get("RUNNER_TEMP", "."), "build-output.log") + + if output_file and args.reviewdog: + append_github_env({"REVIEWDOG_LOG": output_file}) + + print(f"Running: {' '.join(cmd)}") + start = time.monotonic() + + if output_file: + with open(output_file, "w") as log: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + for line in proc.stdout: + sys.stdout.write(line) + log.write(line) + proc.wait() + exit_code = proc.returncode + else: + result = subprocess.run(cmd, check=False) + exit_code = result.returncode + + duration = int(time.monotonic() - start) + + if exit_code == 0: + print(f"::notice::Build completed in {duration}s") + else: + print(f"::error::Build failed after {duration}s") + if not args.continue_on_error: + sys.exit(exit_code) + + +def cmd_configure(args: argparse.Namespace) -> None: + """Run configure.py with standardized arguments.""" + workspace = os.environ.get("GITHUB_WORKSPACE", ".") + cmd = [ + sys.executable, os.path.join(workspace, "tools", "configure.py"), + "-S", args.source_dir, + "-B", args.build_dir, + "-G", args.generator, + "-t", args.build_type, + ] + if args.testing: + cmd.append("--testing") + if args.coverage: + cmd.append("--coverage") + if args.stable: + cmd.append("--stable") + if not args.use_qt_cmake: + cmd.append("--no-qt-cmake") + if args.unity_build: + cmd += ["--unity", "--unity-batch", args.unity_batch_size] + if args.extra_args: + cmd.append("--") + cmd.extend(args.extra_args.split()) + + start = time.monotonic() + result = subprocess.run(cmd, check=False) + duration = int(time.monotonic() - start) + print(f"::notice::Configure completed in {duration}s") + sys.exit(result.returncode) + + +def cmd_ctest(args: argparse.Namespace) -> None: + """Run CTest with standardized arguments and timing.""" + cmd = [ + "ctest", + "--output-on-failure", + "--output-junit", args.junit_output, + "--parallel", str(args.jobs), + ] + if args.include_labels: + cmd += ["-L", args.include_labels] + if args.exclude_labels: + cmd += ["-LE", args.exclude_labels] + + start = time.monotonic() + + with open(args.ctest_output, "w") as log: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + for line in proc.stdout: + sys.stdout.write(line) + log.write(line) + proc.wait() + exit_code = proc.returncode + + duration = int(time.monotonic() - start) + print(f"::notice::Tests completed in {duration}s") + sys.exit(exit_code) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + # detect-jobs + p_jobs = sub.add_parser("detect-jobs") + p_jobs.add_argument("--parallel", default="auto") + + # build + p_build = sub.add_parser("build") + p_build.add_argument("--target", default="") + p_build.add_argument("--build-type", default="") + p_build.add_argument("--parallel", action="store_true", default=False) + p_build.add_argument("--parallel-jobs", default="") + p_build.add_argument("--output-file", default="") + p_build.add_argument("--continue-on-error", action="store_true", default=False) + p_build.add_argument("--reviewdog", action="store_true", default=False) + + # configure + p_conf = sub.add_parser("configure") + p_conf.add_argument("--source-dir", required=True) + p_conf.add_argument("--build-dir", required=True) + p_conf.add_argument("--generator", default="Ninja") + p_conf.add_argument("--build-type", default="Release") + p_conf.add_argument("--testing", action="store_true", default=False) + p_conf.add_argument("--coverage", action="store_true", default=False) + p_conf.add_argument("--stable", action="store_true", default=False) + p_conf.add_argument("--use-qt-cmake", action="store_true", default=True) + p_conf.add_argument("--no-qt-cmake", action="store_false", dest="use_qt_cmake") + p_conf.add_argument("--unity-build", action="store_true", default=False) + p_conf.add_argument("--unity-batch-size", default="16") + p_conf.add_argument("--extra-args", default="") + + # ctest + p_ctest = sub.add_parser("ctest") + p_ctest.add_argument("--junit-output", required=True) + p_ctest.add_argument("--ctest-output", required=True) + p_ctest.add_argument("--jobs", type=int, required=True) + p_ctest.add_argument("--include-labels", default="") + p_ctest.add_argument("--exclude-labels", default="") + + args = parser.parse_args() + commands = { + "detect-jobs": cmd_detect_jobs, + "build": cmd_build, + "configure": cmd_configure, + "ctest": cmd_ctest, + } + commands[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/collect_artifact_sizes.py b/.github/scripts/collect_artifact_sizes.py index fd65b29b33bf..20d9baf98855 100644 --- a/.github/scripts/collect_artifact_sizes.py +++ b/.github/scripts/collect_artifact_sizes.py @@ -14,15 +14,10 @@ ensure_tools_dir(__file__) -from common.gh_actions import list_run_artifacts, list_workflow_runs_for_sha +from common.gh_actions import list_run_artifacts, list_workflow_runs_for_sha, parse_csv_list from common.github_runs import select_latest_runs_by_name -def parse_csv_list(value: str) -> list[str]: - """Parse comma-separated values into a trimmed non-empty list.""" - return [item.strip() for item in value.split(",") if item.strip()] - - _DISTRIBUTABLE_PREFIXES = ( "QGroundControl", "Custom-QGroundControl", diff --git a/.github/scripts/collect_build_status.py b/.github/scripts/collect_build_status.py index 0918b4e5f78c..fea4839964bc 100644 --- a/.github/scripts/collect_build_status.py +++ b/.github/scripts/collect_build_status.py @@ -4,7 +4,6 @@ from __future__ import annotations import argparse -import hashlib import json import os from typing import Any @@ -13,15 +12,10 @@ ensure_tools_dir(__file__) -from common.gh_actions import list_workflow_runs_for_sha +from common.gh_actions import list_workflow_runs_for_sha, parse_csv_list, write_github_output from common.github_runs import select_latest_runs_by_name -def parse_csv_list(value: str) -> list[str]: - """Parse comma-separated values into a trimmed non-empty list.""" - return [item.strip() for item in value.split(",") if item.strip()] - - def latest_runs_by_name( runs: list[dict[str, Any]], names: set[str], @@ -74,22 +68,6 @@ def render_table(platforms: list[str], states: dict[str, dict[str, str]]) -> str return "\n".join(lines) -def write_output(key: str, value: str) -> None: - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - - if "\n" in value: - value_hash = hashlib.sha256(value.encode("utf-8")).hexdigest()[:12] - delim = f"EOF_{key}_{value_hash}" - while delim in value: - delim = f"{delim}_X" - with open(github_output, "a", encoding="utf-8") as f: - f.write(f"{key}<<{delim}\n{value}\n{delim}\n") - else: - with open(github_output, "a", encoding="utf-8") as f: - f.write(f"{key}={value}\n") - def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Collect build status for PR build-results comment.") @@ -138,13 +116,15 @@ def main(argv: list[str] | None = None) -> int: "Some builds failed." if all_complete else "Some builds still in progress." ) - write_output("table", table) - write_output("summary", summary) - write_output("all_complete", "true" if all_complete else "false") - write_output("precommit_status", precommit["status"]) - write_output("precommit_url", precommit["url"]) - write_output("precommit_conclusion", precommit["conclusion"]) - write_output("precommit_run_id", precommit["run_id"]) + write_github_output({ + "table": table, + "summary": summary, + "all_complete": "true" if all_complete else "false", + "precommit_status": precommit["status"], + "precommit_url": precommit["url"], + "precommit_conclusion": precommit["conclusion"], + "precommit_run_id": precommit["run_id"], + }) print(table) print(summary) diff --git a/.github/scripts/deploy_docs.py b/.github/scripts/deploy_docs.py new file mode 100644 index 000000000000..2e19fd85e474 --- /dev/null +++ b/.github/scripts/deploy_docs.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Deploy built documentation to an external GitHub Pages repository.""" + +from __future__ import annotations + +import argparse +import datetime +import re +import shutil +import subprocess +import sys +from pathlib import Path + + +def sanitize_branch(name: str) -> str: + """Remove unsafe characters from branch name for use as a directory.""" + return re.sub(r"[^a-zA-Z0-9._-]", "_", name) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source-dir", required=True, help="Path to built docs") + parser.add_argument("--target-dir", required=True, help="Path to target repo checkout") + parser.add_argument("--branch", required=True, help="Source branch name (used as subdirectory)") + parser.add_argument("--target-branch", default="main", help="Branch to push to") + parser.add_argument("--commit-message", default="Docs update") + parser.add_argument("--author-email", default="bot@px4.io") + parser.add_argument("--author-name", default="PX4BuildBot") + args = parser.parse_args() + + safe_branch = sanitize_branch(args.branch) + target = Path(args.target_dir) + deploy_dir = target / safe_branch + + if deploy_dir.exists(): + shutil.rmtree(deploy_dir) + deploy_dir.mkdir(parents=True) + + source = Path(args.source_dir) + for item in source.iterdir(): + dest = deploy_dir / item.name + if item.is_dir(): + shutil.copytree(item, dest) + else: + shutil.copy2(item, dest) + + def git(*cmd: str) -> subprocess.CompletedProcess: + return subprocess.run(["git", *cmd], cwd=str(target), check=True) + + git("config", "user.email", args.author_email) + git("config", "user.name", args.author_name) + git("add", safe_branch) + + diff = subprocess.run( + ["git", "diff", "--cached", "--quiet"], + cwd=str(target), check=False, + ) + if diff.returncode == 0: + print("No documentation changes to deploy.") + return + + today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d") + git("commit", "-m", f"{args.commit_message} {today}") + git("push", "origin", args.target_branch) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/detect_changes.py b/.github/scripts/detect_changes.py new file mode 100644 index 000000000000..aca58e2ed447 --- /dev/null +++ b/.github/scripts/detect_changes.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Detect whether a CI build is needed based on changed files and platform. + +Computes the git diff for the current event (pull_request, push, merge_group) +and matches changed files against platform-specific patterns. + +Usage: + python3 .github/scripts/detect_changes.py --platform linux +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +from typing import Sequence + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import write_github_output + +# Patterns that trigger a build for ANY platform +_COMMON_PATTERNS: list[str] = [ + r"^src/", + r"^CMakeLists\.txt$", + r"^cmake/", + r"^libs/", + r"^resources/", + r"^translations/", + r"^.*\.qrc$", + r"^test/", + r"^\.github/actions/", + r"^\.github/scripts/", + r"^\.github/build-config\.json$", +] + +# Per-platform additional patterns +_PLATFORM_PATTERNS: dict[str, list[str]] = { + "android": [r"^android/"], + "docker-linux": [ + r"^deploy/docker/Dockerfile-build-ubuntu$", + r"^deploy/linux/", + ], + "docker-android": [ + r"^deploy/docker/Dockerfile-build-android$", + r"^android/", + r"^deploy/android/", + ], +} + +# Per-platform tools/setup patterns +_SETUP_PATTERNS: dict[str, list[str]] = { + "linux": [r"^tools/setup/.*debian", r"^tools/setup/install_dependencies\.py$"], + "windows": [r"^tools/setup/.*windows"], + "macos": [r"^tools/setup/.*macos"], + "android": [r"^tools/setup/"], + "ios": [r"^tools/setup/.*ios", r"^tools/setup/.*macos"], + "docker-linux": [r"^tools/setup/"], + "docker-android": [r"^tools/setup/"], +} + + +def workflow_name_for_platform(platform: str) -> str: + """Map a platform to its workflow YAML filename (without extension).""" + if platform.startswith("docker-"): + return "docker" + return platform + + +def build_patterns(platform: str) -> list[re.Pattern[str]]: + """Build the list of compiled regex patterns for a platform.""" + wf = workflow_name_for_platform(platform) + raw: list[str] = list(_COMMON_PATTERNS) + raw.append(rf"^deploy/{re.escape(platform)}/") + raw.append(rf"^\.github/workflows/{re.escape(wf)}\.yml$") + raw.extend(_PLATFORM_PATTERNS.get(platform, [])) + raw.extend(_SETUP_PATTERNS.get(platform, [])) + return [re.compile(p) for p in raw] + + +def has_relevant_changes(files: Sequence[str], platform: str) -> bool: + """Check if any changed file matches the platform's patterns.""" + patterns = build_patterns(platform) + for f in files: + if not f: + continue + for p in patterns: + if p.search(f): + return True + return False + + +# --------------------------------------------------------------------------- +# Git helpers — these call out to git as subprocesses +# --------------------------------------------------------------------------- + +_NULL_SHA = "0" * 40 + + +def _run_git(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["git", *args], + capture_output=True, + text=True, + check=check, + ) + + +def _ensure_commit(sha: str) -> bool: + """Ensure a commit exists locally; fetch it shallowly if needed.""" + if not sha: + return True + r = _run_git("cat-file", "-e", f"{sha}^{{commit}}", check=False) + if r.returncode == 0: + return True + _run_git("fetch", "--no-tags", "--depth=1", "origin", sha, check=False) + return _run_git("cat-file", "-e", f"{sha}^{{commit}}", check=False).returncode == 0 + + +def _diff_names(sha_a: str, sha_b: str) -> list[str] | None: + """Return changed file names between two commits, or None on failure.""" + r = _run_git("diff", "--name-only", sha_a, sha_b, check=False) + if r.returncode != 0: + return None + return r.stdout.strip().splitlines() + + +def _tree_names(sha: str) -> list[str]: + """List files changed in a single commit.""" + r = _run_git("diff-tree", "--no-commit-id", "--name-only", "-r", sha, check=False) + return r.stdout.strip().splitlines() if r.returncode == 0 else [] + + +def get_changed_files() -> list[str] | None: + """Determine changed files from environment variables set by GitHub Actions. + + Returns None if the diff cannot be computed reliably (caller should force build). + """ + event = os.environ.get("EVENT_NAME", "") + + if event == "pull_request": + base = os.environ.get("PR_BASE_SHA", "") + head = os.environ.get("PR_HEAD_SHA", "") + if not _ensure_commit(base) or not _ensure_commit(head): + return None + return _diff_names(base, head) + + if event == "push": + before = os.environ.get("PUSH_BEFORE_SHA", "") + current = os.environ.get("CURRENT_SHA", "") + if before and before != _NULL_SHA: + if not _ensure_commit(before) or not _ensure_commit(current): + return None + return _diff_names(before, current) + return _tree_names(current) + + if event == "merge_group": + merge_base = os.environ.get("MERGE_BASE_SHA", "") + current = os.environ.get("CURRENT_SHA", "") + if merge_base: + if not _ensure_commit(merge_base) or not _ensure_commit(current): + return None + return _diff_names(merge_base, current) + + current = os.environ.get("CURRENT_SHA", "") + return _tree_names(current) if current else None + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Detect CI-relevant file changes for a platform.") + parser.add_argument("--platform", required=True, + help="Platform (linux, windows, macos, android, ios, docker-linux, docker-android)") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + platform = args.platform + + changed = get_changed_files() + if changed is None: + print("::warning::Unable to compute changed files reliably; forcing build for safety.") + write_github_output({"any": "true"}) + return 0 + + print("Changed files:") + for f in changed: + print(f" {f}") + + any_changed = has_relevant_changes(changed, platform) + write_github_output({"any": "true" if any_changed else "false"}) + print(f"Results: any={any_changed}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/docker_helper.py b/.github/scripts/docker_helper.py new file mode 100644 index 000000000000..e782519b1594 --- /dev/null +++ b/.github/scripts/docker_helper.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Docker build helpers for CI. + +Subcommands: + validate Validate dockerfile and build-type inputs + run Run docker build with retry logic +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +import time + +VALID_DOCKERFILES = {"Dockerfile-build-ubuntu", "Dockerfile-build-android"} +VALID_BUILD_TYPES = {"Release", "Debug"} + + +def cmd_validate(args: argparse.Namespace) -> None: + ok = True + if args.dockerfile not in VALID_DOCKERFILES: + print(f"::error::Invalid dockerfile '{args.dockerfile}'", file=sys.stderr) + print(f"Allowed values: {', '.join(sorted(VALID_DOCKERFILES))}", file=sys.stderr) + ok = False + if args.build_type not in VALID_BUILD_TYPES: + print(f"::error::Invalid build-type '{args.build_type}'", file=sys.stderr) + print(f"Allowed values: {', '.join(sorted(VALID_BUILD_TYPES))}", file=sys.stderr) + ok = False + if not ok: + sys.exit(1) + print(f"Using dockerfile: {args.dockerfile}") + print(f"Build type: {args.build_type}") + + +def cmd_run(args: argparse.Namespace) -> None: + cmd = ["./deploy/docker/docker-run.sh"] + if args.fuse: + cmd.append("--fuse") + cmd += [args.image, args.build_type] + + for attempt in range(1, args.max_attempts + 1): + result = subprocess.run(cmd, check=False) + if result.returncode == 0: + return + if attempt >= args.max_attempts: + print(f"::error::Docker build failed after {attempt} attempt(s).", file=sys.stderr) + sys.exit(1) + print(f"Docker build failed on attempt {attempt}. Retrying in {args.retry_delay} seconds...") + time.sleep(args.retry_delay) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + p_val = sub.add_parser("validate") + p_val.add_argument("--dockerfile", required=True) + p_val.add_argument("--build-type", required=True) + + p_run = sub.add_parser("run") + p_run.add_argument("--image", default="qgc-builder:latest") + p_run.add_argument("--build-type", required=True) + p_run.add_argument("--fuse", action="store_true", default=False) + p_run.add_argument("--max-attempts", type=int, default=2) + p_run.add_argument("--retry-delay", type=int, default=30) + + args = parser.parse_args() + {"validate": cmd_validate, "run": cmd_run}[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/tools/setup/download_artifacts.py b/.github/scripts/download_artifacts.py old mode 100755 new mode 100644 similarity index 80% rename from tools/setup/download_artifacts.py rename to .github/scripts/download_artifacts.py index 3a3dc59a8db4..812c4543aece --- a/tools/setup/download_artifacts.py +++ b/.github/scripts/download_artifacts.py @@ -5,7 +5,7 @@ from all successful platform builds for a given commit SHA. Usage: - python3 tools/setup/download_artifacts.py \\ + python3 .github/scripts/download_artifacts.py \\ --repo owner/repo \\ --head-sha abc123 \\ --output-dir artifacts \\ @@ -17,19 +17,17 @@ import argparse import concurrent.futures -from datetime import datetime import json -import subprocess import sys from pathlib import Path from typing import Any -REPO_ROOT = Path(__file__).resolve().parents[2] -COMMON_DIR = REPO_ROOT / "tools" / "common" -if str(COMMON_DIR) not in sys.path: - sys.path.insert(0, str(COMMON_DIR)) +from ci_bootstrap import ensure_tools_dir -import gh_actions as _gh_actions +ensure_tools_dir(__file__) + +from common.gh_actions import gh, list_workflow_runs_for_sha, list_run_artifacts +from common.github_runs import group_runs_by_name, select_latest_runs_by_name ARTIFACT_EXTENSIONS = ( @@ -45,33 +43,6 @@ ARTIFACT_EXTENSION_SET = frozenset(ARTIFACT_EXTENSIONS) -def gh(*args: str, check: bool = True) -> subprocess.CompletedProcess: - """Run a gh CLI command and return the result.""" - return _gh_actions.gh(*args, check=check) - - -def _parse_created_at(created_at: Any) -> datetime | None: - value = str(created_at).strip() - if not value: - return None - if value.endswith("Z"): - value = f"{value[:-1]}+00:00" - try: - return datetime.fromisoformat(value) - except ValueError: - return None - - -def _is_newer_run(candidate: dict[str, Any], existing: dict[str, Any]) -> bool: - candidate_created_at = str(candidate.get("created_at", "")) - existing_created_at = str(existing.get("created_at", "")) - candidate_dt = _parse_created_at(candidate_created_at) - existing_dt = _parse_created_at(existing_created_at) - if candidate_dt is not None and existing_dt is not None: - return candidate_dt > existing_dt - return candidate_created_at > existing_created_at - - def select_latest_successful_runs( runs: list[dict[str, Any]], workflows: list[str], @@ -79,21 +50,13 @@ def select_latest_successful_runs( event: str = "", ) -> list[dict[str, Any]]: """Return latest completed/successful run per workflow name.""" - workflow_set = set(workflows) - latest_by_workflow: dict[str, dict[str, Any]] = {} - - for run in runs: - name = str(run.get("name", "")) - if name not in workflow_set: - continue - if run.get("status") != "completed" or run.get("conclusion") != "success": - continue - if event and run.get("event") != event: - continue - existing = latest_by_workflow.get(name) - if existing is None or _is_newer_run(run, existing): - latest_by_workflow[name] = run - + latest_by_workflow = select_latest_runs_by_name( + runs, + set(workflows), + event=event, + status="completed", + conclusion="success", + ) order = {name: idx for idx, name in enumerate(workflows)} return sorted( latest_by_workflow.values(), @@ -108,40 +71,24 @@ def group_successful_runs_by_workflow( event: str = "", ) -> dict[str, list[dict[str, Any]]]: """Group successful runs by workflow name, newest first.""" - workflow_set = set(workflows) - grouped: dict[str, list[dict[str, Any]]] = {name: [] for name in workflows} - - for run in runs: - name = str(run.get("name", "")) - if name not in workflow_set: - continue - if run.get("status") != "completed" or run.get("conclusion") != "success": - continue - if event and run.get("event") != event: - continue - grouped[name].append(run) - - for workflow_name in grouped: - grouped[workflow_name].sort( - key=lambda run: ( - _parse_created_at(run.get("created_at")) is not None, - str(run.get("created_at", "")), - ), - reverse=True, - ) - - return grouped + return group_runs_by_name( + runs, + workflows, + event=event, + status="completed", + conclusion="success", + ) def get_workflow_runs(repo: str, head_sha: str, workflows: list[str], *, event: str = "") -> list[dict]: """Return completed successful runs for the given workflows and commit.""" - all_runs = _gh_actions.list_workflow_runs_for_sha(repo, head_sha) + all_runs = list_workflow_runs_for_sha(repo, head_sha) return select_latest_successful_runs(all_runs, workflows, event=event) def select_artifact_names_for_run(repo: str, run_id: int, prefixes: list[str]) -> list[str]: """Return artifact names that match configured name prefixes.""" - return select_artifact_names(_gh_actions.list_run_artifacts(repo, run_id), prefixes) + return select_artifact_names(list_run_artifacts(repo, run_id), prefixes) def select_artifact_names(artifacts: list[dict[str, Any]], prefixes: list[str]) -> list[str]: @@ -280,7 +227,7 @@ def main(argv: list[str] | None = None) -> int: return 1 all_runs = loaded_runs else: - all_runs = _gh_actions.list_workflow_runs_for_sha(repo, head_sha) + all_runs = list_workflow_runs_for_sha(repo, head_sha) preloaded_artifacts: dict[int, list[dict[str, Any]]] = {} had_successful_runs = bool(select_latest_successful_runs(all_runs, workflows, event=event)) if artifact_prefixes: @@ -291,7 +238,7 @@ def main(argv: list[str] | None = None) -> int: selected_run: dict[str, Any] | None = None for candidate in candidates: run_id = int(candidate["id"]) - artifacts = _gh_actions.list_run_artifacts(repo, run_id) + artifacts = list_run_artifacts(repo, run_id) preloaded_artifacts[run_id] = artifacts if select_artifact_names(artifacts, artifact_prefixes): selected_run = candidate @@ -332,7 +279,7 @@ def main(argv: list[str] | None = None) -> int: if artifact_prefixes or artifact_metadata_out is not None: artifacts = preloaded_artifacts.get(run_id) if artifacts is None: - artifacts = _gh_actions.list_run_artifacts(repo, run_id) + artifacts = list_run_artifacts(repo, run_id) if artifact_prefixes: names = select_artifact_names(artifacts, artifact_prefixes) run_artifact_map[run_id] = names if names else None diff --git a/.github/scripts/generate_build_results_comment.py b/.github/scripts/generate_build_results_comment.py index a8a5106b8978..b61d4ba2bf83 100644 --- a/.github/scripts/generate_build_results_comment.py +++ b/.github/scripts/generate_build_results_comment.py @@ -10,13 +10,17 @@ import re from datetime import UTC, datetime from pathlib import Path -from typing import Mapping +from typing import Any, Mapping from urllib.parse import quote, urlsplit, urlunsplit +import jinja2 + from xml_utils import XMLParseError, xml_parse as _xml_parse logger = logging.getLogger(__name__) +_TEMPLATE_DIR = Path(__file__).parent / "templates" + def _env(env: Mapping[str, str], key: str, default: str = "") -> str: return str(env.get(key, default)).strip() @@ -104,101 +108,80 @@ def _failed_test_lines(content: str, limit: int = 20) -> list[str]: return lines -def _render_test_results(base_dir: Path, env: Mapping[str, str]) -> list[str]: +def _format_delta_mb(delta_bytes: int) -> str: + delta_mb = delta_bytes / 1024.0 / 1024.0 + if delta_bytes > 0: + return f"+{delta_mb:.2f} MB (increase)" + if delta_bytes < 0: + return f"{delta_mb:.2f} MB (decrease)" + return "No change" + + +def _format_size_human(size_bytes: int) -> str: + size_mb = size_bytes / 1024.0 / 1024.0 + if size_mb >= 1024: + return f"{(size_mb / 1024):.2f} GB" + return f"{size_mb:.2f} MB" + + +def _collect_test_data(base_dir: Path, env: Mapping[str, str]) -> dict[str, Any] | None: pattern = _env(env, "TEST_RESULTS_GLOB", "artifacts/test-results-*/test-output.txt") files = sorted(base_dir.glob(pattern)) if not files: - return [] + return None - lines = ["### Test Results", ""] - total_passed = 0 - total_failed = 0 - total_skipped = 0 + platforms: list[dict[str, Any]] = [] + total_passed = total_failed = total_skipped = 0 has_failures = False for file in files: arch = file.parent.name.removeprefix("test-results-") content = file.read_text(encoding="utf-8", errors="ignore") passed, failed, skipped = _count_test_results(content) - total_passed += passed total_failed += failed total_skipped += skipped + entry: dict[str, Any] = {"arch": arch, "passed": passed, "failed": failed, "skipped": skipped} if failed > 0: has_failures = True - lines.append(f"**{arch}**: {passed} passed, {failed} failed, {skipped} skipped") - lines.append("") - lines.append("
    Failed tests") - lines.append("") - lines.append("```") - lines.extend(_failed_test_lines(content)) - lines.append("```") - lines.append("
    ") - else: - lines.append(f"**{arch}**: {passed} passed, {skipped} skipped") - lines.append("") + entry["failed_lines"] = _failed_test_lines(content) + platforms.append(entry) - if has_failures: - lines.append(f"**Total: {total_passed} passed, {total_failed} failed, {total_skipped} skipped**") - else: - lines.append(f"**Total: {total_passed} passed, {total_skipped} skipped**") - lines.append("") - return lines + return { + "platforms": platforms, + "total_passed": total_passed, + "total_failed": total_failed, + "total_skipped": total_skipped, + "has_failures": has_failures, + } -def _render_coverage(base_dir: Path, env: Mapping[str, str]) -> list[str]: +def _collect_coverage_data(base_dir: Path, env: Mapping[str, str]) -> dict[str, Any] | None: coverage_path = base_dir / _env(env, "COVERAGE_XML", "artifacts/coverage-report/coverage.xml") if not coverage_path.exists(): - return [] + return None - lines = ["### Code Coverage", ""] current = _parse_coverage_percent(coverage_path) baseline_path = base_dir / _env(env, "BASELINE_COVERAGE_XML", "baseline-coverage.xml") baseline = _parse_coverage_percent(baseline_path) - coverage_text = f"{current:.1f}%" if current is not None else "N/A" - + result: dict[str, Any] = {"current": current, "baseline": baseline} if baseline is not None and current is not None: - diff = current - baseline - lines.append("| Coverage | Baseline | Change |") - lines.append("|----------|----------|--------|") - lines.append(f"| {coverage_text} | {baseline:.1f}% | {diff:+.1f}% |") - else: - lines.append(f"Coverage: **{coverage_text}**") - lines.append("") - lines.append("*No baseline available for comparison*") - - lines.append("") - return lines - - -def _format_delta_mb(delta_bytes: int) -> str: - delta_mb = delta_bytes / 1024.0 / 1024.0 - if delta_bytes > 0: - return f"+{delta_mb:.2f} MB (increase)" - if delta_bytes < 0: - return f"{delta_mb:.2f} MB (decrease)" - return "No change" - - -def _format_size_human(size_bytes: int) -> str: - size_mb = size_bytes / 1024.0 / 1024.0 - if size_mb >= 1024: - return f"{(size_mb / 1024):.2f} GB" - return f"{size_mb:.2f} MB" + result["diff"] = current - baseline + return result -def _render_artifact_sizes(base_dir: Path, env: Mapping[str, str]) -> list[str]: +def _collect_artifact_data(base_dir: Path, env: Mapping[str, str]) -> dict[str, Any] | None: pr_sizes_path = base_dir / _env(env, "PR_SIZES_JSON", "pr-sizes.json") if not pr_sizes_path.exists(): - return [] + return None try: pr_data = json.loads(pr_sizes_path.read_text(encoding="utf-8")) except Exception: logger.debug("Failed to parse PR sizes from %s", pr_sizes_path, exc_info=True) - return [] + return None baseline_path = base_dir / _env(env, "BASELINE_SIZES_JSON", "baseline-sizes.json") baseline: dict[str, int] = {} @@ -219,19 +202,12 @@ def _render_artifact_sizes(base_dir: Path, env: Mapping[str, str]) -> list[str]: logger.debug("Failed to parse baseline sizes from %s", baseline_path, exc_info=True) baseline = {} - lines = ["### Artifact Sizes", ""] - if baseline: - lines.append("| Artifact | Size | Δ from master |") - lines.append("|----------|------|---------------|") - else: - lines.append("| Artifact | Size |") - lines.append("|----------|------|") - - total_delta = 0 artifacts = pr_data.get("artifacts", []) if not isinstance(artifacts, list): - return [] + return None + items: list[dict[str, Any]] = [] + total_delta = 0 for artifact in artifacts: if not isinstance(artifact, dict): continue @@ -244,21 +220,20 @@ def _render_artifact_sizes(base_dir: Path, env: Mapping[str, str]) -> list[str]: continue size_human = str(artifact.get("size_human", "")).strip() or _format_size_human(new_size) + entry: dict[str, Any] = {"name": name, "size_human": size_human} if baseline and name in baseline: - old_size = baseline[name] - delta = new_size - old_size + delta = new_size - baseline[name] total_delta += delta - lines.append(f"| {name} | {size_human} | {_format_delta_mb(delta)} |") - else: - lines.append(f"| {name} | {size_human} |") - - lines.append("") - if baseline and total_delta != 0: - direction = "increased" if total_delta > 0 else "decreased" - lines.append(f"**Total size {direction} by {abs(total_delta) / 1024.0 / 1024.0:.2f} MB**") - elif not baseline: - lines.append("*No baseline available for comparison*") - return lines + entry["delta"] = delta + entry["delta_human"] = _format_delta_mb(delta) + items.append(entry) + + return { + "entries": items, + "has_baseline": bool(baseline), + "total_delta": total_delta, + "total_delta_mb": abs(total_delta) / 1024.0 / 1024.0, + } def generate_comment(env: Mapping[str, str], base_dir: Path, now_utc: datetime | None = None) -> str: @@ -280,50 +255,41 @@ def generate_comment(env: Mapping[str, str], base_dir: Path, now_utc: datetime | if parsed_note: precommit_note = parsed_note - lines: list[str] = [ - "## Build Results", - "", - "### Platform Status", - "", - ] + now = now_utc or datetime.now(UTC) - if table: - lines.extend(table.strip().splitlines()) - else: - lines.extend(["| Platform | Status | Details |", "|----------|--------|--------|"]) - - lines.extend( - [ - "", - f"**{summary}**", - "", - "### Pre-commit", - "", - "| Check | Status | Details |", - "|-------|--------|---------|", - f"| pre-commit | {precommit_status} | {precommit_details} |", - "", - ] + jinja_env = jinja2.Environment( + loader=jinja2.FileSystemLoader(_TEMPLATE_DIR), + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, ) - - if precommit_note: - lines.append(precommit_note) - lines.append("") - - lines.extend(_render_test_results(base_dir, env)) - lines.extend(_render_coverage(base_dir, env)) - lines.extend(_render_artifact_sizes(base_dir, env)) - - now = now_utc or datetime.now(UTC) - lines.extend( - [ - "", - "---", - f"Updated: {now.strftime('%Y-%m-%d %H:%M:%S UTC')} • Triggered by: {triggered_by}", - ] + template = jinja_env.get_template("build_results.md.j2") + + rendered = template.render( + table=table, + summary=summary, + precommit={"status": precommit_status, "details": precommit_details, "note": precommit_note}, + tests=_collect_test_data(base_dir, env), + coverage=_collect_coverage_data(base_dir, env), + artifacts=_collect_artifact_data(base_dir, env), + timestamp=now.strftime("%Y-%m-%d %H:%M:%S UTC"), + triggered_by=triggered_by, ) - return "\n".join(lines).rstrip() + "\n" + # Normalize: collapse 3+ blank lines to 2, strip trailing whitespace per line + lines = [line.rstrip() for line in rendered.splitlines()] + result: list[str] = [] + blank_count = 0 + for line in lines: + if not line: + blank_count += 1 + if blank_count <= 2: + result.append(line) + else: + blank_count = 0 + result.append(line) + + return "\n".join(result).rstrip() + "\n" def parse_args() -> argparse.Namespace: diff --git a/.github/scripts/gstreamer_archive.py b/.github/scripts/gstreamer_archive.py index 84beaef7aa70..6a0c796208b2 100755 --- a/.github/scripts/gstreamer_archive.py +++ b/.github/scripts/gstreamer_archive.py @@ -20,6 +20,15 @@ from dataclasses import dataclass from pathlib import Path +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import ( # noqa: E402 + write_github_output as _write_github_output, + write_step_summary as _write_step_summary, +) + @dataclass class ArchiveResult: @@ -258,18 +267,16 @@ def _install_aws_cli_macos(self) -> None: run_command(["sudo", "installer", "-pkg", str(installer_pkg), "-target", "/"]) def write_github_output(self) -> None: - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output or not self._archive_result: + if not self._archive_result: return - - with open(github_output, "a") as f: - f.write(f"name={self._archive_result.name}\n") - f.write(f"path={self._archive_result.path}\n") - f.write(f"ext={self._archive_result.extension}\n") + _write_github_output({ + "name": self._archive_result.name, + "path": str(self._archive_result.path), + "ext": self._archive_result.extension, + }) def write_github_summary(self, uploaded: bool = False) -> None: - summary_path = os.environ.get("GITHUB_STEP_SUMMARY") - if not summary_path or not self._archive_result: + if not self._archive_result: return arch_display = self._normalize_arch() @@ -294,8 +301,7 @@ def write_github_summary(self, uploaded: bool = False) -> None: f"| S3 Path | dependencies/gstreamer/{self.platform}/{self.version}/ |" ) - with open(summary_path, "a") as f: - f.write("\n".join(lines) + "\n") + _write_step_summary("\n".join(lines) + "\n") def parse_args() -> argparse.Namespace: diff --git a/.github/scripts/install_dependencies_helper.py b/.github/scripts/install_dependencies_helper.py new file mode 100644 index 000000000000..c48cd67e1f9e --- /dev/null +++ b/.github/scripts/install_dependencies_helper.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Post-install fixups for CI dependency caching on Linux. + +Handles: +- Enabling the Ubuntu universe repository +- Repairing apt alternatives after cache-apt-pkgs-action restore +- Installing optional packages +- Detecting Python version for pipx cache keys +""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import write_github_output # noqa: E402 + +APT_OPTS = ["-o", "DPkg::Lock::Timeout=300", "-o", "Acquire::Retries=3"] + + +def _run(cmd: list[str], *, check: bool = True, **kwargs) -> subprocess.CompletedProcess: + return subprocess.run(cmd, check=check, **kwargs) + + +def _sudo(cmd: list[str], **kwargs) -> subprocess.CompletedProcess: + return _run(["sudo", *cmd], **kwargs) + + +def _ldconfig_has_blas() -> bool: + result = _run(["ldconfig", "-p"], capture_output=True, text=True, check=False) + return bool(re.search(r"\blibblas\.so\.3\b", result.stdout)) + + +def _get_multiarch() -> str: + result = _run( + ["dpkg-architecture", "-qDEB_HOST_MULTIARCH"], + capture_output=True, text=True, check=False, + ) + return result.stdout.strip() if result.returncode == 0 else "" + + +def enable_universe() -> None: + """Enable the Ubuntu universe repository if not already present.""" + sources_dirs = [Path("/etc/apt/sources.list"), Path("/etc/apt/sources.list.d")] + universe_found = False + + for source in sources_dirs: + if not source.exists(): + continue + paths = [source] if source.is_file() else list(source.glob("*.list")) + list(source.glob("*.sources")) + for p in paths: + try: + text = p.read_text(errors="replace") + if re.search(r"^(deb|Components:).*universe", text, re.MULTILINE): + universe_found = True + break + except OSError: + continue + if universe_found: + break + + if not universe_found: + _sudo(["apt-get", *APT_OPTS, "install", "-y", "-qq", "software-properties-common"]) + _sudo(["add-apt-repository", "-y", "universe"]) + + _sudo(["apt-get", *APT_OPTS, "update", "-y", "-qq"]) + + +def fix_apt_alternatives() -> None: + """Repair apt alternatives and BLAS symlinks after cache restore.""" + result = _sudo(["dpkg", "--configure", "-a"], check=False) + if result.returncode != 0: + print("::warning::dpkg --configure -a failed; package state may be inconsistent") + + if _ldconfig_has_blas(): + return + + print("::warning::libblas.so.3 missing from linker cache; attempting repair") + _sudo( + ["apt-get", *APT_OPTS, "install", "-y", "-qq", "--reinstall", "libblas3", "libopenblas0"], + check=False, + ) + + multiarch = _get_multiarch() + if multiarch: + _sudo(["update-alternatives", "--auto", f"libblas.so.3-{multiarch}"], check=False) + + blas_link = Path(f"/usr/lib/{multiarch}/libblas.so.3") + if not blas_link.exists(): + candidates = list(Path("/usr/lib").rglob("libblas.so.3")) + if candidates: + candidate = candidates[0] + print(f"::warning::Creating compatibility symlink {blas_link} -> {candidate}") + _sudo(["ln", "-sf", str(candidate), str(blas_link)]) + + _sudo(["ldconfig"]) + + if not _ldconfig_has_blas(): + print("::error::libblas.so.3 is still missing after repair attempt") + sys.exit(1) + + +def install_optional_packages() -> None: + """Install optional apt packages that are available.""" + result = _run( + [sys.executable, "tools/setup/install_dependencies.py", + "--platform", "debian", "--category", "gstreamer_optional", "--print-available-packages"], + capture_output=True, text=True, check=False, + ) + packages = result.stdout.strip() + if packages: + print(f"Installing optional packages: {packages}") + _sudo(["apt-get", *APT_OPTS, "install", "-y", "-qq", *packages.split()], check=False) + + +def detect_python_version() -> None: + """Write Python minor version to GITHUB_OUTPUT for cache keys.""" + minor = f"{sys.version_info.major}.{sys.version_info.minor}" + write_github_output({"minor": minor}) + + +def main() -> None: + import argparse + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + sub.add_parser("enable-universe") + sub.add_parser("fix-apt-alternatives") + sub.add_parser("install-optional") + sub.add_parser("detect-python-version") + + args = parser.parse_args() + commands = { + "enable-universe": enable_universe, + "fix-apt-alternatives": fix_apt_alternatives, + "install-optional": install_optional_packages, + "detect-python-version": detect_python_version, + } + commands[args.command]() + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/plan_docker_builds.py b/.github/scripts/plan_docker_builds.py index d2f428f5fa85..723f2f5c9851 100644 --- a/.github/scripts/plan_docker_builds.py +++ b/.github/scripts/plan_docker_builds.py @@ -7,7 +7,12 @@ import json import os import sys -from pathlib import Path + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import write_github_output def plan_builds(event_name: str, linux_changed: bool, android_changed: bool) -> dict[str, object]: @@ -39,20 +44,12 @@ def plan_builds(event_name: str, linux_changed: bool, android_changed: bool) -> return {"matrix": {"include": include}, "has_jobs": bool(include)} -def write_github_output(path: Path, values: dict[str, str]) -> None: - """Append key/value outputs for GitHub Actions.""" - with path.open("a", encoding="utf-8") as handle: - for key, value in values.items(): - handle.write(f"{key}={value}\n") - - def parse_args(argv: list[str] | None = None) -> argparse.Namespace: """Parse command-line arguments.""" parser = argparse.ArgumentParser(description="Plan Docker workflow builds.") parser.add_argument("--event-name", default=os.environ.get("EVENT_NAME", "")) parser.add_argument("--linux", default=os.environ.get("LINUX", "false")) parser.add_argument("--android", default=os.environ.get("ANDROID", "false")) - parser.add_argument("--github-output", default=os.environ.get("GITHUB_OUTPUT", "")) return parser.parse_args(argv) @@ -67,14 +64,10 @@ def main(argv: list[str] | None = None) -> int: matrix_json = json.dumps(plan["matrix"], separators=(",", ":")) print(matrix_json) - if args.github_output: - write_github_output( - Path(args.github_output), - { - "matrix": matrix_json, - "has_jobs": "true" if plan["has_jobs"] else "false", - }, - ) + write_github_output({ + "matrix": matrix_json, + "has_jobs": "true" if plan["has_jobs"] else "false", + }) return 0 diff --git a/.github/scripts/resolve_gstreamer_config.py b/.github/scripts/resolve_gstreamer_config.py new file mode 100644 index 000000000000..ef9fadf29cd7 --- /dev/null +++ b/.github/scripts/resolve_gstreamer_config.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Resolve GStreamer and Qt versions for GitHub Actions.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.build_config import get_build_config_value +from common.gh_actions import write_github_output + +VERSION_KEYS = { + "macos": "gstreamer_macos_version", + "windows": "gstreamer_windows_version", + "android": "gstreamer_android_version", + "ios": "gstreamer_ios_version", +} + + +def resolve_version(platform_name: str, requested_version: str) -> str: + """Return explicit version override or platform-specific config version.""" + if requested_version: + return requested_version + version_key = VERSION_KEYS.get(platform_name, "gstreamer_default_version") + return get_build_config_value(version_key) + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Resolve GStreamer action config values.") + parser.add_argument("--platform", required=True, choices=["linux", "macos", "windows", "android", "ios"]) + parser.add_argument("--version", default="", help="Explicit GStreamer version override") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Resolve config values and emit GitHub outputs.""" + args = parse_args(argv) + outputs = { + "py_cmd": "python3", + "version": resolve_version(args.platform, args.version), + "qt_version": get_build_config_value("qt_version"), + } + write_github_output(outputs) + print(f"Building GStreamer {outputs['version']} for {args.platform}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/size_analysis.py b/.github/scripts/size_analysis.py index 4f31f2b6cd72..367d94c63e0b 100755 --- a/.github/scripts/size_analysis.py +++ b/.github/scripts/size_analysis.py @@ -21,6 +21,15 @@ from pathlib import Path from typing import Any +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import ( # noqa: E402 + write_github_output as _write_github_output, + write_step_summary as _write_step_summary, +) + class BinaryAnalyzer: """Analyzes binary size and symbol information.""" @@ -215,20 +224,16 @@ def generate_summary( def write_github_output(binary_size: int, stripped_size: int, symbol_count: int) -> None: """Write outputs to GITHUB_OUTPUT file if available.""" - github_output = os.environ.get("GITHUB_OUTPUT") - if github_output: - with open(github_output, "a") as f: - f.write(f"binary_size={binary_size}\n") - f.write(f"stripped_size={stripped_size}\n") - f.write(f"symbol_count={symbol_count}\n") + _write_github_output({ + "binary_size": str(binary_size), + "stripped_size": str(stripped_size), + "symbol_count": str(symbol_count), + }) def write_github_step_summary(summary: str) -> None: """Write summary to GITHUB_STEP_SUMMARY file if available.""" - step_summary = os.environ.get("GITHUB_STEP_SUMMARY") - if step_summary: - with open(step_summary, "a") as f: - f.write(summary) + _write_step_summary(summary) def parse_args() -> argparse.Namespace: diff --git a/.github/scripts/templates/build_results.md.j2 b/.github/scripts/templates/build_results.md.j2 new file mode 100644 index 000000000000..d100b4bf2c91 --- /dev/null +++ b/.github/scripts/templates/build_results.md.j2 @@ -0,0 +1,89 @@ +## Build Results + +### Platform Status + +{% if table -%} +{{ table }} +{% else -%} +| Platform | Status | Details | +|----------|--------|--------| +{% endif %} + +**{{ summary }}** + +### Pre-commit + +| Check | Status | Details | +|-------|--------|---------| +| pre-commit | {{ precommit.status }} | {{ precommit.details }} | + +{% if precommit.note -%} +{{ precommit.note }} + +{% endif -%} +{% if tests -%} +### Test Results + +{% for t in tests.platforms -%} +{% if t.failed > 0 -%} +**{{ t.arch }}**: {{ t.passed }} passed, {{ t.failed }} failed, {{ t.skipped }} skipped + +
    Failed tests + +``` +{% for line in t.failed_lines -%} +{{ line }} +{% endfor -%} +``` +
    +{% else -%} +**{{ t.arch }}**: {{ t.passed }} passed, {{ t.skipped }} skipped +{% endif %} +{% endfor -%} +{% if tests.has_failures -%} +**Total: {{ tests.total_passed }} passed, {{ tests.total_failed }} failed, {{ tests.total_skipped }} skipped** +{% else -%} +**Total: {{ tests.total_passed }} passed, {{ tests.total_skipped }} skipped** +{% endif %} +{% endif -%} +{% if coverage -%} +### Code Coverage + +{% if coverage.baseline is not none and coverage.current is not none -%} +| Coverage | Baseline | Change | +|----------|----------|--------| +| {{ "%.1f"|format(coverage.current) }}% | {{ "%.1f"|format(coverage.baseline) }}% | {{ "%+.1f"|format(coverage.diff) }}% | +{% else -%} +Coverage: **{% if coverage.current is not none %}{{ "%.1f"|format(coverage.current) }}%{% else %}N/A{% endif %}** + +*No baseline available for comparison* +{% endif %} +{% endif -%} +{% if artifacts -%} +### Artifact Sizes + +{% if artifacts.has_baseline -%} +| Artifact | Size | Δ from master | +|----------|------|---------------| +{% for a in artifacts.entries -%} +{% if a.delta is defined -%} +| {{ a.name }} | {{ a.size_human }} | {{ a.delta_human }} | +{% else -%} +| {{ a.name }} | {{ a.size_human }} | +{% endif -%} +{% endfor %} +{% else -%} +| Artifact | Size | +|----------|------| +{% for a in artifacts.entries -%} +| {{ a.name }} | {{ a.size_human }} | +{% endfor %} +{% endif -%} +{% if artifacts.has_baseline and artifacts.total_delta != 0 -%} +**Total size {{ "increased" if artifacts.total_delta > 0 else "decreased" }} by {{ "%.2f"|format(artifacts.total_delta_mb) }} MB** +{%- elif not artifacts.has_baseline -%} +*No baseline available for comparison* +{%- endif %} +{% endif %} +--- +Updated: {{ timestamp }} • Triggered by: {{ triggered_by }} diff --git a/.github/scripts/test_duration_report.py b/.github/scripts/test_duration_report.py index 5ea44097ff5f..fcda4e756c57 100644 --- a/.github/scripts/test_duration_report.py +++ b/.github/scripts/test_duration_report.py @@ -11,6 +11,8 @@ from pathlib import Path from typing import Any +from xml_utils import xml_parse + def test_key(elem: ET.Element) -> str: """Return a stable identifier for a JUnit testcase element.""" @@ -64,7 +66,7 @@ def analyze_test_durations( min_delta: float, ) -> dict[str, Any]: """Return computed timing report data for a JUnit XML file.""" - tree = ET.parse(junit_path) + tree = xml_parse(junit_path) root = tree.getroot() cases = [(test_key(testcase), parse_time(testcase.attrib.get("time", "0"))) for testcase in root.iter("testcase")] diff --git a/.github/scripts/tests/test_aws_upload.py b/.github/scripts/tests/test_aws_upload.py new file mode 100644 index 000000000000..5317897a81b5 --- /dev/null +++ b/.github/scripts/tests/test_aws_upload.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Tests for aws_upload.py.""" + +from __future__ import annotations + +import pytest + +from aws_upload import sanitize_ref, validate_artifact, validate_credentials + + +class TestSanitizeRef: + def test_simple_branch(self) -> None: + assert sanitize_ref("main") == "main" + + def test_slashes_replaced(self) -> None: + assert sanitize_ref("feature/foo") == "feature_foo" + + def test_dotdot_removed(self) -> None: + assert sanitize_ref("../../../etc/passwd") == "___etc_passwd" + + def test_special_chars(self) -> None: + assert sanitize_ref("v1.2.3-rc1") == "v1.2.3-rc1" + + def test_spaces_replaced(self) -> None: + assert sanitize_ref("my branch") == "my_branch" + + +class TestValidateCredentials: + def test_role_arn_sufficient(self) -> None: + validate_credentials("arn:aws:iam::123:role/test", "", "") + + def test_static_creds_sufficient(self) -> None: + validate_credentials("", "AKID", "secret") + + def test_missing_both_exits(self) -> None: + with pytest.raises(SystemExit): + validate_credentials("", "", "") + + def test_partial_static_exits(self) -> None: + with pytest.raises(SystemExit): + validate_credentials("", "AKID", "") + + +class TestValidateArtifact: + def test_path_traversal_rejected(self, tmp_path) -> None: + f = tmp_path / "test.bin" + f.touch() + with pytest.raises(SystemExit): + validate_artifact(str(f), "../test.bin") + + def test_slash_in_name_rejected(self, tmp_path) -> None: + f = tmp_path / "test.bin" + f.touch() + with pytest.raises(SystemExit): + validate_artifact(str(f), "path/test.bin") + + def test_backslash_rejected(self, tmp_path) -> None: + f = tmp_path / "test.bin" + f.touch() + with pytest.raises(SystemExit): + validate_artifact(str(f), "path\\test.bin") + + def test_missing_file_exits(self) -> None: + with pytest.raises(SystemExit): + validate_artifact("/nonexistent/file", "test.bin") + + def test_valid_artifact(self, tmp_path) -> None: + f = tmp_path / "QGroundControl.AppImage" + f.touch() + validate_artifact(str(f), "QGroundControl.AppImage") diff --git a/.github/scripts/tests/test_check_baseline_ready.py b/.github/scripts/tests/test_check_baseline_ready.py index 65d8f20aeedb..b175a478e170 100644 --- a/.github/scripts/tests/test_check_baseline_ready.py +++ b/.github/scripts/tests/test_check_baseline_ready.py @@ -7,7 +7,7 @@ import pytest from check_baseline_ready import evaluate_readiness -from check_baseline_ready import write_output +from common.gh_actions import write_github_output PLATFORMS = ["Linux", "Windows", "MacOS", "Android"] @@ -84,7 +84,7 @@ def test_write_output_multiline_uses_collision_resistant_delimiter( monkeypatch.setenv("GITHUB_OUTPUT", str(out_path)) value = "line1\nEOF_missing_deadbeef\ntail" - write_output("missing", value) + write_github_output({"missing": value}) text = out_path.read_text(encoding="utf-8") assert "missing< None: + assert detect_jobs("4") == 4 + + def test_explicit_value_large(self) -> None: + assert detect_jobs("16") == 16 + + def test_auto_uses_cpu_count(self) -> None: + with patch("os.cpu_count", return_value=8): + assert detect_jobs("auto") == 8 + + def test_auto_fallback_none(self) -> None: + with patch("os.cpu_count", return_value=None): + assert detect_jobs("auto") == 2 + + def test_invalid_exits(self) -> None: + import pytest + with pytest.raises(SystemExit): + detect_jobs("abc") + + def test_zero_exits(self) -> None: + import pytest + with pytest.raises(SystemExit): + detect_jobs("0") + + def test_negative_exits(self) -> None: + import pytest + with pytest.raises(SystemExit): + detect_jobs("-1") diff --git a/.github/scripts/tests/test_collect_build_status.py b/.github/scripts/tests/test_collect_build_status.py index a45f054af055..53c491e75e18 100644 --- a/.github/scripts/tests/test_collect_build_status.py +++ b/.github/scripts/tests/test_collect_build_status.py @@ -7,6 +7,7 @@ import pytest import collect_build_status as mod +from common.gh_actions import write_github_output def _run( @@ -84,7 +85,7 @@ def test_write_output_uses_collision_resistant_delimiter(tmp_path: Path, monkeyp monkeypatch.setenv("GITHUB_OUTPUT", str(out_path)) value = "line1\nEOF_table_deadbeef\ntail" - mod.write_output("table", value) + write_github_output({"table": value}) text = out_path.read_text(encoding="utf-8") assert "table< None: + assert sanitize_branch("main") == "main" + + def test_slashes(self) -> None: + assert sanitize_branch("feature/docs-update") == "feature_docs-update" + + def test_special_chars(self) -> None: + assert sanitize_branch("v1.2.3-rc1") == "v1.2.3-rc1" + + def test_spaces(self) -> None: + assert sanitize_branch("my branch") == "my_branch" + + def test_preserves_dots_dashes_underscores(self) -> None: + assert sanitize_branch("release_v2.0-beta") == "release_v2.0-beta" diff --git a/.github/scripts/tests/test_detect_changes.py b/.github/scripts/tests/test_detect_changes.py new file mode 100644 index 000000000000..babdd2d3f29e --- /dev/null +++ b/.github/scripts/tests/test_detect_changes.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Tests for detect_changes.py.""" + +from __future__ import annotations + +import pytest + +from detect_changes import ( + build_patterns, + has_relevant_changes, + workflow_name_for_platform, +) + + +class TestWorkflowNameForPlatform: + def test_regular_platform(self) -> None: + assert workflow_name_for_platform("linux") == "linux" + assert workflow_name_for_platform("windows") == "windows" + assert workflow_name_for_platform("macos") == "macos" + + def test_docker_prefix(self) -> None: + assert workflow_name_for_platform("docker-linux") == "docker" + assert workflow_name_for_platform("docker-android") == "docker" + + +class TestBuildPatterns: + def test_common_patterns_present(self) -> None: + patterns = build_patterns("linux") + pattern_strs = [p.pattern for p in patterns] + assert any("^src/" in p for p in pattern_strs) + assert any("CMakeLists" in p for p in pattern_strs) + + def test_platform_workflow_pattern(self) -> None: + patterns = build_patterns("linux") + pattern_strs = [p.pattern for p in patterns] + assert any("linux\\.yml" in p for p in pattern_strs) + + def test_docker_workflow_pattern(self) -> None: + patterns = build_patterns("docker-linux") + pattern_strs = [p.pattern for p in patterns] + assert any("docker\\.yml" in p for p in pattern_strs) + + def test_android_extra_pattern(self) -> None: + patterns = build_patterns("android") + pattern_strs = [p.pattern for p in patterns] + assert any("^android/" in p for p in pattern_strs) + + def test_deploy_pattern_uses_platform(self) -> None: + patterns = build_patterns("linux") + pattern_strs = [p.pattern for p in patterns] + assert any("deploy/linux/" in p for p in pattern_strs) + + +class TestHasRelevantChanges: + def test_src_change_triggers_any_platform(self) -> None: + assert has_relevant_changes(["src/Vehicle.cc"], "linux") + assert has_relevant_changes(["src/Vehicle.cc"], "windows") + assert has_relevant_changes(["src/Vehicle.cc"], "android") + + def test_cmakelists_triggers(self) -> None: + assert has_relevant_changes(["CMakeLists.txt"], "linux") + + def test_cmake_dir_triggers(self) -> None: + assert has_relevant_changes(["cmake/FindFoo.cmake"], "macos") + + def test_qrc_triggers(self) -> None: + assert has_relevant_changes(["resources/qgc.qrc"], "linux") + + def test_unrelated_file_does_not_trigger(self) -> None: + assert not has_relevant_changes(["README.md"], "linux") + assert not has_relevant_changes(["docs/guide.md"], "windows") + + def test_workflow_file_triggers_own_platform(self) -> None: + assert has_relevant_changes([".github/workflows/linux.yml"], "linux") + assert not has_relevant_changes([".github/workflows/linux.yml"], "windows") + + def test_actions_dir_triggers_all(self) -> None: + assert has_relevant_changes([".github/actions/qt-install/action.yml"], "linux") + assert has_relevant_changes([".github/actions/qt-install/action.yml"], "android") + + def test_build_config_triggers_all(self) -> None: + assert has_relevant_changes([".github/build-config.json"], "ios") + + def test_android_dir_only_triggers_android(self) -> None: + assert has_relevant_changes(["android/AndroidManifest.xml"], "android") + assert not has_relevant_changes(["android/AndroidManifest.xml"], "linux") + + def test_docker_linux_patterns(self) -> None: + assert has_relevant_changes(["deploy/docker/Dockerfile-build-ubuntu"], "docker-linux") + assert has_relevant_changes(["deploy/linux/AppImage.sh"], "docker-linux") + assert not has_relevant_changes(["deploy/docker/Dockerfile-build-ubuntu"], "linux") + + def test_docker_android_patterns(self) -> None: + assert has_relevant_changes(["deploy/docker/Dockerfile-build-android"], "docker-android") + assert has_relevant_changes(["android/build.gradle"], "docker-android") + + def test_setup_patterns_linux(self) -> None: + assert has_relevant_changes(["tools/setup/install_dependencies.py"], "linux") + assert has_relevant_changes(["tools/setup/foo-debian.sh"], "linux") + assert not has_relevant_changes(["tools/setup/foo-windows.ps1"], "linux") + + def test_setup_patterns_windows(self) -> None: + assert has_relevant_changes(["tools/setup/foo-windows.ps1"], "windows") + assert not has_relevant_changes(["tools/setup/foo-debian.sh"], "windows") + + def test_setup_patterns_android_matches_all(self) -> None: + assert has_relevant_changes(["tools/setup/anything.py"], "android") + + def test_setup_patterns_ios(self) -> None: + assert has_relevant_changes(["tools/setup/foo-ios.sh"], "ios") + assert has_relevant_changes(["tools/setup/foo-macos.sh"], "ios") + assert not has_relevant_changes(["tools/setup/foo-debian.sh"], "ios") + + def test_empty_files_list(self) -> None: + assert not has_relevant_changes([], "linux") + + def test_empty_strings_ignored(self) -> None: + assert not has_relevant_changes(["", ""], "linux") + + def test_test_dir_triggers(self) -> None: + assert has_relevant_changes(["test/UnitTest.cc"], "linux") + + def test_deploy_platform_triggers(self) -> None: + assert has_relevant_changes(["deploy/macos/Info.plist"], "macos") + assert not has_relevant_changes(["deploy/macos/Info.plist"], "linux") + + def test_scripts_dir_triggers_all(self) -> None: + assert has_relevant_changes([".github/scripts/foo.py"], "windows") + + def test_docker_workflow_triggers_docker_platforms(self) -> None: + assert has_relevant_changes([".github/workflows/docker.yml"], "docker-linux") + assert has_relevant_changes([".github/workflows/docker.yml"], "docker-android") + assert not has_relevant_changes([".github/workflows/docker.yml"], "linux") diff --git a/.github/scripts/tests/test_docker_helper.py b/.github/scripts/tests/test_docker_helper.py new file mode 100644 index 000000000000..af855eb71bd5 --- /dev/null +++ b/.github/scripts/tests/test_docker_helper.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Tests for docker_helper.py.""" + +from __future__ import annotations + +import argparse + +import pytest + +from docker_helper import VALID_BUILD_TYPES, VALID_DOCKERFILES, cmd_validate + + +class TestValidate: + def test_valid_ubuntu_release(self) -> None: + args = argparse.Namespace(dockerfile="Dockerfile-build-ubuntu", build_type="Release") + cmd_validate(args) + + def test_valid_android_debug(self) -> None: + args = argparse.Namespace(dockerfile="Dockerfile-build-android", build_type="Debug") + cmd_validate(args) + + def test_invalid_dockerfile(self) -> None: + args = argparse.Namespace(dockerfile="Dockerfile-bad", build_type="Release") + with pytest.raises(SystemExit): + cmd_validate(args) + + def test_invalid_build_type(self) -> None: + args = argparse.Namespace(dockerfile="Dockerfile-build-ubuntu", build_type="BadType") + with pytest.raises(SystemExit): + cmd_validate(args) diff --git a/tools/tests/test_download_artifacts.py b/.github/scripts/tests/test_download_artifacts.py similarity index 88% rename from tools/tests/test_download_artifacts.py rename to .github/scripts/tests/test_download_artifacts.py index 0eabb4d67c9d..98990f6ea1e7 100644 --- a/tools/tests/test_download_artifacts.py +++ b/.github/scripts/tests/test_download_artifacts.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Tests for tools/setup/download_artifacts.py.""" +"""Tests for download_artifacts.py.""" from __future__ import annotations @@ -8,7 +8,7 @@ from pathlib import Path from unittest.mock import patch -from setup import download_artifacts as mod +import download_artifacts as mod def _cp(stdout: str = "", stderr: str = "", returncode: int = 0) -> subprocess.CompletedProcess: @@ -34,7 +34,7 @@ def test_get_workflow_runs_filters_by_name_status_and_conclusion() -> None: {"id": 4, "name": "Other", "status": "completed", "conclusion": "success"}, ] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux", "Windows"]) assert len(runs) == 1 @@ -67,7 +67,7 @@ def test_get_workflow_runs_keeps_latest_successful_run_per_workflow() -> None: }, ] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux", "Windows"]) by_name = {run["name"]: run["id"] for run in runs} @@ -81,7 +81,7 @@ def test_get_workflow_runs_filters_by_event() -> None: {"id": 11, "name": "Linux", "status": "completed", "conclusion": "success", "event": "pull_request"}, ] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux"], event="pull_request") assert [run["id"] for run in runs] == [11] @@ -116,7 +116,7 @@ def test_select_artifact_names_for_run_filters_by_prefix() -> None: {"name": "emulator-diagnostics-123"}, {"name": "QGroundControl-x86_64.AppImage"}, ] - with patch.object(mod._gh_actions, "list_run_artifacts", return_value=artifacts): + with patch.object(mod, "list_run_artifacts", return_value=artifacts): names = mod.select_artifact_names_for_run( "owner/repo", 42, @@ -138,7 +138,7 @@ def test_list_downloaded_artifacts_filters_extensions(tmp_path: Path) -> None: def test_main_returns_zero_when_no_runs_found() -> None: - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=[]), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=[]), patch.object( mod, "select_latest_successful_runs", return_value=[], ): rc = mod.main(["--repo", "owner/repo", "--head-sha", "abc123"]) @@ -147,7 +147,7 @@ def test_main_returns_zero_when_no_runs_found() -> None: def test_main_returns_one_when_downloads_fail_and_no_files(tmp_path: Path) -> None: runs = [{"id": 42, "name": "Linux", "status": "completed", "conclusion": "success"}] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( mod, "download_run_artifacts", return_value=False, @@ -165,10 +165,10 @@ def test_main_returns_one_when_downloads_fail_and_no_files(tmp_path: Path) -> No def test_main_returns_two_when_no_artifacts_match_prefixes(tmp_path: Path) -> None: runs = [{"id": 42, "name": "Linux", "status": "completed", "conclusion": "success"}] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( - mod._gh_actions, "list_run_artifacts", return_value=[{"name": "unrelated-artifact", "size_in_bytes": 1}], + mod, "list_run_artifacts", return_value=[{"name": "unrelated-artifact", "size_in_bytes": 1}], ), patch.object( mod, "download_run_artifacts", return_value=True, ), patch.object( @@ -242,8 +242,8 @@ def _artifacts_for_run(repo: str, run_id: int) -> list[dict[str, object]]: downloaded = tmp_path / "coverage.xml" downloaded.write_text("", encoding="utf-8") - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( - mod._gh_actions, "list_run_artifacts", side_effect=_artifacts_for_run, + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( + mod, "list_run_artifacts", side_effect=_artifacts_for_run, ), patch.object( mod, "download_run_artifacts", return_value=True, ) as download_mock, patch.object( @@ -273,10 +273,10 @@ def test_main_writes_artifact_metadata_file(tmp_path: Path) -> None: downloaded = tmp_path / "dummy.txt" downloaded.write_text("x", encoding="utf-8") - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( - mod._gh_actions, "list_run_artifacts", return_value=artifacts, + mod, "list_run_artifacts", return_value=artifacts, ), patch.object( mod, "download_run_artifacts", return_value=True, ), patch.object( diff --git a/.github/scripts/tests/test_resolve_gstreamer_config.py b/.github/scripts/tests/test_resolve_gstreamer_config.py new file mode 100644 index 000000000000..af1d56bad700 --- /dev/null +++ b/.github/scripts/tests/test_resolve_gstreamer_config.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Tests for resolve_gstreamer_config.py.""" + +from __future__ import annotations + +from unittest.mock import patch + +from resolve_gstreamer_config import resolve_version + + +def test_resolve_version_prefers_explicit_override() -> None: + assert resolve_version("linux", "1.2.3") == "1.2.3" + + +def test_resolve_version_uses_platform_specific_key() -> None: + with patch("resolve_gstreamer_config.get_build_config_value", return_value="9.9.9") as mock_get: + assert resolve_version("windows", "") == "9.9.9" + mock_get.assert_called_once() diff --git a/.github/scripts/verify_executable.py b/.github/scripts/verify_executable.py new file mode 100644 index 000000000000..efaf89dabe53 --- /dev/null +++ b/.github/scripts/verify_executable.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Verify QGroundControl executable with a boot test. + +Resolves paths, sets permissions, and delegates to run_tests.py --verify-only. +""" + +from __future__ import annotations + +import argparse +import os +import stat +import subprocess +import sys +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--binary-path", required=True) + parser.add_argument("--working-dir", default="") + parser.add_argument("--type", default="binary", dest="exe_type") + parser.add_argument("--timeout", default="60") + args = parser.parse_args() + + binary_path = Path(args.binary_path) + work_dir = Path(args.working_dir) if args.working_dir else binary_path.parent + + if not work_dir.is_dir(): + print(f"::error::Working directory not found: {work_dir}", file=sys.stderr) + sys.exit(1) + + binary_name = binary_path.name + run_binary = binary_name + headless = True + + # AppImages typically don't ship the "offscreen" Qt platform plugin. + if args.exe_type == "appimage" or binary_name.endswith(".AppImage"): + headless = False + + if os.name != "nt": + exe = work_dir / binary_name + try: + exe.chmod(exe.stat().st_mode | stat.S_IEXEC) + except OSError: + pass + run_binary = str(exe.resolve()) + + workspace = os.environ.get("GITHUB_WORKSPACE", ".") + cmd = [ + sys.executable, os.path.join(workspace, "tools", "run_tests.py"), + "--binary", run_binary, + "--timeout", args.timeout, + "--verify-only", + "-v", + ] + if headless: + cmd.append("--headless") + + result = subprocess.run(cmd, cwd=str(work_dir), check=False) + sys.exit(result.returncode) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/_detect-changes.yml b/.github/workflows/_detect-changes.yml new file mode 100644 index 000000000000..9712e51fe396 --- /dev/null +++ b/.github/workflows/_detect-changes.yml @@ -0,0 +1,39 @@ +name: Detect Changes + +on: + workflow_call: + inputs: + platform: + description: 'Platform to detect changes for' + required: true + type: string + outputs: + should_build: + description: 'Whether relevant files changed' + value: ${{ jobs.detect.outputs.should_build }} + +concurrency: + group: detect-changes-${{ inputs.platform }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + detect: + name: Detect Changes + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + should_build: ${{ steps.detect.outputs.any }} + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + fetch-depth: 1 + - name: Detect changes + id: detect + uses: ./.github/actions/detect-changes + with: + platform: ${{ inputs.platform }} diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 10b17b2ae139..97815662b29f 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -49,10 +49,14 @@ jobs: with: fetch-depth: 1 - - name: Get build config (clazy/iwyu) + - name: Build Setup (clazy/iwyu) if: contains(fromJSON('["clazy", "iwyu"]'), inputs.tool) - id: config - uses: ./.github/actions/build-config + uses: ./.github/actions/build-setup + with: + qt-host: linux + qt-arch: linux_gcc_64 + build-type: Debug + save-cache: true - name: Prepare apt repositories (clazy) if: inputs.tool == 'clazy' @@ -74,34 +78,12 @@ jobs: packages: clazy ninja-build version: clazy-linux-v1 - - name: Initial Setup (clazy/iwyu) - if: contains(fromJSON('["clazy", "iwyu"]'), inputs.tool) - uses: ./.github/actions/common - - name: Install Dependencies (iwyu) if: inputs.tool == 'iwyu' uses: ./.github/actions/install-dependencies with: extra-packages: iwyu clang ninja-build - - name: Setup Caching (iwyu) - if: inputs.tool == 'iwyu' - uses: ./.github/actions/cache - with: - host: linux - target: linux_gcc_64 - build-type: Debug - save-cache: true - - - name: Install Qt (clazy/iwyu) - if: contains(fromJSON('["clazy", "iwyu"]'), inputs.tool) - uses: ./.github/actions/qt-install - with: - version: ${{ steps.config.outputs.qt_version }} - host: linux - arch: linux_gcc_64 - modules: ${{ steps.config.outputs.qt_modules }} - - name: Build Setup (sanitizers) if: contains(fromJSON('["asan", "ubsan", "tsan"]'), inputs.tool) uses: ./.github/actions/build-setup diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index cae1dbc45aa3..10bf550bb455 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -31,27 +31,13 @@ permissions: id-token: write attestations: write actions: read + security-events: write jobs: changes: - name: Detect Changes - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - outputs: - should_build: ${{ steps.detect.outputs.any }} - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Detect changes - id: detect - uses: ./.github/actions/detect-changes - with: - platform: android + uses: ./.github/workflows/_detect-changes.yml + with: + platform: android build: name: Android (${{ matrix.host }}) @@ -109,7 +95,6 @@ jobs: steps: - name: Harden Runner - if: runner.os == 'Linux' uses: step-security/harden-runner@v2 with: egress-policy: audit @@ -133,6 +118,7 @@ jobs: cache-read-only: ${{ github.event_name == 'pull_request' }} - name: Install Qt for Android + id: qt-android uses: ./.github/actions/qt-android with: host: ${{ matrix.qt_host || matrix.host }} @@ -147,7 +133,6 @@ jobs: android-platform: ${{ steps.config.outputs.android_platform }} android-cmdline-tools: ${{ steps.config.outputs.android_cmdline_tools }} android-build-tools: ${{ steps.config.outputs.android_build_tools }} - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Create Debug Keystore if: ${{ env.QT_ANDROID_KEYSTORE_STORE_PASS == '' || !matrix.primary }} @@ -171,10 +156,13 @@ jobs: build-dir: ${{ runner.temp }}/build build-type: ${{ inputs.build_type || 'Release' }} stable: ${{ (github.ref_type == 'tag' || contains(github.ref, 'Stable')) && 'true' || 'false' }} + use-qt-cmake: 'false' extra-args: >- -DCMAKE_WARN_DEPRECATED=FALSE + -DCMAKE_TOOLCHAIN_FILE=${{ steps.qt-android.outputs.target_qt_root_dir }}/lib/cmake/Qt6/qt.toolchain.cmake + -DCMAKE_PREFIX_PATH=${{ steps.qt-android.outputs.target_qt_root_dir }} -DQT_ANDROID_ABIS=${{ env.QT_ANDROID_ABIS }} - -DQT_HOST_PATH=${{ env.QT_ROOT_DIR }}/../${{ matrix.qt_host_path }} + -DQT_HOST_PATH=${{ steps.qt-android.outputs.host_qt_root_dir }} -DQT_ANDROID_SIGN_APK=ON - name: Build @@ -371,25 +359,18 @@ jobs: if-no-files-found: warn retention-days: 14 - - name: Attest Build with SBOM + - name: Attest and Upload if: ${{ !matrix.emulator }} - uses: ./.github/actions/attest-sbom + uses: ./.github/actions/attest-and-upload with: - subject-path: ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk + artifact-name: ${{ env.PACKAGE }}.apk + package-name: ${{ matrix.primary && env.PACKAGE || format('{0}-{1}', env.PACKAGE, matrix.host) }} subject-name: ${{ env.PACKAGE }}-${{ matrix.host }} - scan-path: ${{ runner.temp }}/build - - - name: Upload Build File - if: ${{ !matrix.emulator }} - uses: ./.github/actions/upload - with: - artifact_name: ${{ env.PACKAGE }}.apk - package_name: ${{ matrix.primary && env.PACKAGE || format('{0}-{1}', env.PACKAGE, matrix.host) }} - aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} - aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} - upload_aws: ${{ matrix.primary }} + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-distribution-id: ${{ secrets.AWS_DISTRIBUTION_ID }} + upload-aws: ${{ matrix.primary }} - name: Deploy to Play Store if: ${{ !matrix.emulator && matrix.primary && github.ref_type == 'tag' && env.HAS_PLAYSTORE_SECRET == 'true' }} diff --git a/.github/workflows/build-results.yml b/.github/workflows/build-results.yml index dfc5c9b07c04..c8c7c5c00847 100644 --- a/.github/workflows/build-results.yml +++ b/.github/workflows/build-results.yml @@ -56,19 +56,59 @@ jobs: const exact = prs.find(pr => pr.head?.sha === run.head_sha); return exact ? exact.number : null; - - name: Checkout + - name: Check if all platform workflows are complete + id: gate if: steps.pr.outputs.result != 'null' + uses: actions/github-script@v8 + env: + PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + with: + script: | + const wanted = new Set(process.env.PLATFORM_WORKFLOWS.split(',').map(s => s.trim())); + const runs = await github.paginate( + github.rest.actions.listWorkflowRunsForRepo, + { + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: process.env.HEAD_SHA, + event: 'pull_request', + per_page: 100 + } + ); + const latest = new Map(); + for (const run of runs) { + if (!wanted.has(run.name)) continue; + const prev = latest.get(run.name); + if (!prev || new Date(run.created_at) > new Date(prev.created_at)) { + latest.set(run.name, run); + } + } + const missing = [...wanted].filter(n => !latest.has(n)); + const incomplete = [...latest.values()].filter(r => r.status !== 'completed'); + const ready = missing.length === 0 && incomplete.length === 0; + if (!ready) { + const reasons = []; + if (missing.length) reasons.push(`missing: ${missing.join(', ')}`); + if (incomplete.length) reasons.push(`still running: ${incomplete.map(r => r.name).join(', ')}`); + core.info(`Skipping early — ${reasons.join('; ')}`); + } + core.setOutput('ready', ready ? 'true' : 'false'); + + - name: Checkout + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' uses: actions/checkout@v6 with: sparse-checkout: | .github/actions/download-all-artifacts .github/actions/collect-artifact-sizes + .github/actions/setup-python .github/scripts/collect_build_status.py .github/scripts/collect_artifact_sizes.py .github/scripts/ci_bootstrap.py .github/scripts/generate_build_results_comment.py + .github/scripts/templates/build_results.md.j2 .github/scripts/xml_utils.py - .github/actions/setup-python tools/common/build_config.py tools/common/file_traversal.py tools/common/gh_actions.py @@ -77,11 +117,17 @@ jobs: tools/uv.lock tools/setup/setup_bootstrap.py tools/setup/install_python.py - tools/setup/download_artifacts.py + .github/scripts/download_artifacts.py sparse-checkout-cone-mode: false + - name: Setup Python dependencies + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' + uses: ./.github/actions/setup-python + with: + groups: scripts + - name: Collect build status - if: steps.pr.outputs.result != 'null' + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' id: builds env: PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }} @@ -145,14 +191,8 @@ jobs: path: baseline-coverage.xml key: coverage-baseline-latest - - name: Setup Python dependencies - if: steps.pr.outputs.result != 'null' - uses: ./.github/actions/setup-python - with: - groups: scripts - - name: Generate combined report - if: steps.pr.outputs.result != 'null' + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' env: BUILD_TABLE: ${{ steps.builds.outputs.table }} BUILD_SUMMARY: ${{ steps.builds.outputs.summary }} @@ -172,7 +212,7 @@ jobs: cat comment.md - name: Deduplicate prior build-results comments - if: steps.pr.outputs.result != 'null' + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' uses: actions/github-script@v8 env: PR_NUMBER: ${{ steps.pr.outputs.result }} @@ -207,7 +247,7 @@ jobs: } - name: Post combined comment - if: steps.pr.outputs.result != 'null' + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' uses: thollander/actions-comment-pull-request@v3 with: pr-number: ${{ steps.pr.outputs.result }} @@ -229,15 +269,54 @@ jobs: contents: read steps: + - name: Check if all platform workflows succeeded + id: gate + uses: actions/github-script@v8 + env: + PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + with: + script: | + const wanted = new Set(process.env.PLATFORM_WORKFLOWS.split(',').map(s => s.trim())); + const runs = await github.paginate( + github.rest.actions.listWorkflowRunsForRepo, + { + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: process.env.HEAD_SHA, + event: 'push', + per_page: 100 + } + ); + const latest = new Map(); + for (const run of runs) { + if (!wanted.has(run.name)) continue; + const prev = latest.get(run.name); + if (!prev || new Date(run.created_at) > new Date(prev.created_at)) { + latest.set(run.name, run); + } + } + const missing = [...wanted].filter(n => !latest.has(n)); + const failed = [...latest.values()].filter(r => r.status !== 'completed' || r.conclusion !== 'success'); + const ready = missing.length === 0 && failed.length === 0; + if (!ready) { + const reasons = []; + if (missing.length) reasons.push(`missing: ${missing.join(', ')}`); + if (failed.length) reasons.push(`not successful: ${failed.map(r => `${r.name} (${r.status}/${r.conclusion})`).join(', ')}`); + core.info(`Skipping baseline save — ${reasons.join('; ')}`); + } + core.setOutput('ready', ready ? 'true' : 'false'); + - name: Checkout + if: steps.gate.outputs.ready == 'true' uses: actions/checkout@v6 with: sparse-checkout: | .github/actions/download-all-artifacts .github/actions/collect-artifact-sizes .github/actions/setup-python - .github/scripts/collect_artifact_sizes.py .github/scripts/check_baseline_ready.py + .github/scripts/collect_artifact_sizes.py .github/scripts/ci_bootstrap.py tools/common/build_config.py tools/common/file_traversal.py @@ -247,10 +326,17 @@ jobs: tools/uv.lock tools/setup/setup_bootstrap.py tools/setup/install_python.py - tools/setup/download_artifacts.py + .github/scripts/download_artifacts.py sparse-checkout-cone-mode: false + - name: Setup Python dependencies + if: steps.gate.outputs.ready == 'true' + uses: ./.github/actions/setup-python + with: + groups: scripts + - name: Verify all platform workflows succeeded for baseline SHA + if: steps.gate.outputs.ready == 'true' id: ready env: PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }} diff --git a/.github/workflows/ci-scripts.yml b/.github/workflows/ci-scripts.yml index b9c2b674d79d..2e3012dd3db5 100644 --- a/.github/workflows/ci-scripts.yml +++ b/.github/workflows/ci-scripts.yml @@ -116,7 +116,7 @@ jobs: - name: Setup Python uses: ./.github/actions/setup-python with: - groups: test + groups: scripts,test python-version: '3.12' - name: Python syntax smoke test diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cd95a562254a..429fa750f1cd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -64,6 +64,8 @@ jobs: fetch-depth: 1 sparse-checkout: | .github/scripts/plan_docker_builds.py + .github/scripts/ci_bootstrap.py + tools/common/gh_actions.py sparse-checkout-cone-mode: false - name: Plan matrix diff --git a/.github/workflows/docs_deploy.yml b/.github/workflows/docs_deploy.yml index 31cf10caacd9..47963303727d 100644 --- a/.github/workflows/docs_deploy.yml +++ b/.github/workflows/docs_deploy.yml @@ -14,6 +14,10 @@ on: - 'package*.json' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read @@ -66,35 +70,12 @@ jobs: with: egress-policy: audit - - name: Download Artifact - uses: actions/download-artifact@v8 - with: - name: qgc_docs_build - path: ~/_book - - - name: Checkout docs repo - uses: actions/checkout@v6 - with: - repository: mavlink/docs.qgroundcontrol.com - token: ${{ secrets.PX4BUILDBOT_ACCESSTOKEN }} - path: docs.qgroundcontrol.com - - name: Deploy docs - env: - BRANCH: ${{ env.BRANCH_NAME }} - run: | - # Sanitize branch name to prevent path traversal and shell injection - SAFE_BRANCH="${BRANCH//[^a-zA-Z0-9._-]/_}" - rm -rf "docs.qgroundcontrol.com/${SAFE_BRANCH}" - mkdir -p "docs.qgroundcontrol.com/${SAFE_BRANCH}" - cp -r ~/_book/* "docs.qgroundcontrol.com/${SAFE_BRANCH}/" - cd docs.qgroundcontrol.com - git config user.email "bot@px4.io" - git config user.name "PX4BuildBot" - git add "${SAFE_BRANCH}" - if git diff --cached --quiet; then - echo "No documentation changes to deploy." - exit 0 - fi - git commit -m "QGC docs build update $(date -u +%Y-%m-%d)" - git push origin master + uses: ./.github/actions/deploy-docs + with: + artifact-name: qgc_docs_build + target-repo: mavlink/docs.qgroundcontrol.com + target-branch: master + deploy-token: ${{ secrets.PX4BUILDBOT_ACCESSTOKEN }} + source-branch: ${{ env.BRANCH_NAME }} + commit-message: 'QGC docs build update' diff --git a/.github/workflows/doxygen_deploy.yml b/.github/workflows/doxygen_deploy.yml index e375bbc50d6a..0b470526bf1c 100644 --- a/.github/workflows/doxygen_deploy.yml +++ b/.github/workflows/doxygen_deploy.yml @@ -55,34 +55,12 @@ jobs: with: egress-policy: audit - - name: Download Artifact - uses: actions/download-artifact@v8 - with: - name: doxygen_build - path: ~/_api_docs - - - name: Checkout API docs repo - uses: actions/checkout@v6 - with: - repository: mavlink/api.qgroundcontrol.com - token: ${{ secrets.PX4BUILDBOT_ACCESSTOKEN }} - path: api.qgroundcontrol.com - - name: Deploy docs - env: - BRANCH: ${{ env.BRANCH_NAME }} - run: | - SAFE_BRANCH="${BRANCH//[^a-zA-Z0-9._-]/_}" - rm -rf "api.qgroundcontrol.com/${SAFE_BRANCH}" - mkdir -p "api.qgroundcontrol.com/${SAFE_BRANCH}" - cp -r ~/_api_docs/* "api.qgroundcontrol.com/${SAFE_BRANCH}/" - cd api.qgroundcontrol.com - git config user.email "bot@px4.io" - git config user.name "PX4BuildBot" - git add "${SAFE_BRANCH}" - if git diff --cached --quiet; then - echo "No documentation changes to deploy." - exit 0 - fi - git commit -m "QGC Doxygen API docs update $(date -u +%Y-%m-%d)" - git push origin main + uses: ./.github/actions/deploy-docs + with: + artifact-name: doxygen_build + target-repo: mavlink/api.qgroundcontrol.com + target-branch: main + deploy-token: ${{ secrets.PX4BUILDBOT_ACCESSTOKEN }} + source-branch: ${{ env.BRANCH_NAME }} + commit-message: 'QGC Doxygen API docs update' diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 9f5d5e661cd5..3452977cf0ea 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -13,6 +13,7 @@ permissions: id-token: write attestations: write actions: read + security-events: write jobs: build: @@ -58,7 +59,6 @@ jobs: xcode-version: ${{ steps.config.outputs.xcode_ios_version }} cpm-modules: ${{ runner.temp }}/build/cpm_modules build-type: ${{ matrix.build_type }} - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Configure uses: ./.github/actions/cmake-configure @@ -74,17 +74,10 @@ jobs: build-dir: ${{ runner.temp }}/build build-type: ${{ matrix.build_type }} - - name: Attest Build with SBOM + - name: Attest and Upload if: matrix.build_type == 'Release' - uses: ./.github/actions/attest-sbom + uses: ./.github/actions/attest-and-upload with: - subject-path: ${{ runner.temp }}/build/${{ env.PACKAGE }}.app - subject-name: ${{ env.PACKAGE }} - scan-path: ${{ runner.temp }}/build - - - name: Upload Build File - uses: ./.github/actions/upload - with: - artifact_name: ${{ env.PACKAGE }}.app - package_name: ${{ env.PACKAGE }} - upload_aws: 'false' + artifact-name: ${{ env.PACKAGE }}.app + package-name: ${{ env.PACKAGE }} + upload-aws: 'false' diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index a56ba18c624c..43f49a58edf9 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -36,24 +36,9 @@ permissions: jobs: changes: - name: Detect Changes - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - outputs: - should_build: ${{ steps.detect.outputs.any }} - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Detect changes - id: detect - uses: ./.github/actions/detect-changes - with: - platform: linux + uses: ./.github/workflows/_detect-changes.yml + with: + platform: linux build: name: Build ${{ matrix.arch }} ${{ matrix.build_type }} @@ -88,6 +73,7 @@ jobs: steps: - name: Harden Runner + if: runner.arch != 'ARM64' uses: step-security/harden-runner@v2 with: egress-policy: audit @@ -104,7 +90,6 @@ jobs: qt-host: ${{ matrix.host }} qt-arch: ${{ matrix.arch }} build-type: ${{ matrix.build_type }} - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Install Dependencies uses: ./.github/actions/install-dependencies @@ -195,24 +180,16 @@ jobs: binary-path: ${{ runner.temp }}/build/${{ matrix.package }}.AppImage type: appimage - - name: Attest Build with SBOM + - name: Attest and Upload if: matrix.build_type == 'Release' - uses: ./.github/actions/attest-sbom + uses: ./.github/actions/attest-and-upload with: - subject-path: ${{ runner.temp }}/build/${{ matrix.package }}.AppImage - subject-name: ${{ matrix.package }} - scan-path: ${{ runner.temp }}/build - - - name: Upload Build File - if: matrix.build_type == 'Release' - uses: ./.github/actions/upload - with: - artifact_name: ${{ matrix.package }}.AppImage - package_name: ${{ matrix.package }} - aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} - aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} + artifact-name: ${{ matrix.package }}.AppImage + package-name: ${{ matrix.package }} + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-distribution-id: ${{ secrets.AWS_DISTRIBUTION_ID }} debug-validation: name: ${{ matrix.job_name }} @@ -263,7 +240,6 @@ jobs: qt-host: linux qt-arch: linux_gcc_64 build-type: Debug - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Install Dependencies uses: ./.github/actions/install-dependencies @@ -433,10 +409,12 @@ jobs: branch_thresh="${branch_thresh:-20}" python3 - "$line_thresh" "$branch_thresh" <<'PY' - import sys - import xml.etree.ElementTree as ET + import os, sys from pathlib import Path + sys.path.insert(0, os.path.join(os.environ["GITHUB_WORKSPACE"], ".github", "scripts")) + from xml_utils import xml_parse + line_thresh = float(sys.argv[1]) branch_thresh = float(sys.argv[2]) @@ -444,7 +422,7 @@ jobs: print("::warning::coverage.xml not found, skipping threshold check") sys.exit(0) - tree = ET.parse("coverage.xml") + tree = xml_parse("coverage.xml") cov = tree.getroot() lines_valid = int(cov.get("lines-valid", 0)) line_rate = float(cov.get("line-rate", 0)) * 100 @@ -478,117 +456,3 @@ jobs: sys.exit(1 if failed else 0) PY - size-metrics: - name: Size Metrics - if: ${{ !cancelled() && (github.event_name != 'workflow_dispatch' || inputs.build_type != 'Debug') }} - runs-on: ubuntu-24.04 - timeout-minutes: 10 - needs: build - permissions: - actions: write - contents: read - pull-requests: write - - steps: - - name: Harden Runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Download metrics artifact - uses: actions/download-artifact@v8 - with: - name: size-metrics - path: . - - - name: Save baseline - if: github.event_name == 'push' && github.ref == 'refs/heads/master' - run: mkdir -p baseline && cp metrics.json baseline/metrics.json - - name: Delete stale baseline cache - if: github.event_name == 'push' && github.ref == 'refs/heads/master' && hashFiles('baseline/metrics.json') != '' - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - run: gh actions-cache delete size-baseline-latest -R "${GH_REPO}" --confirm || true - - - name: Save baseline to cache - uses: actions/cache/save@v5 - if: github.event_name == 'push' && github.ref == 'refs/heads/master' && hashFiles('baseline/metrics.json') != '' - with: - path: baseline/metrics.json - key: size-baseline-latest - - - name: Restore baseline - if: github.event_name == 'pull_request' - id: baseline - uses: actions/cache/restore@v5 - with: - path: baseline/metrics.json - key: size-baseline-latest - - - name: Compare with baseline - if: github.event_name == 'pull_request' && hashFiles('baseline/metrics.json') != '' - uses: actions/github-script@v8 - with: - script: | - if (!context.issue.number) { - core.info('No PR number available; skipping size comparison comment.'); - return; - } - const fs = require('fs'); - const current = JSON.parse(fs.readFileSync('metrics.json', 'utf8')); - const baseline = JSON.parse(fs.readFileSync('baseline/metrics.json', 'utf8')); - - const find = (arr, name) => arr.find(m => m.name === name)?.value || 0; - const fmt = (b) => (b / 1048576).toFixed(2); - - const metrics = ['Binary Size', 'Stripped Size'].map(name => { - const cur = find(current, name); - const base = find(baseline, name); - const delta = base > 0 ? ((cur - base) / base * 100).toFixed(1) : 'N/A'; - return { name, current: cur, baseline: base, delta }; - }); - - const rows = metrics.map(m => - `| ${m.name} | ${fmt(m.baseline)} MB | ${fmt(m.current)} MB | ${m.delta === 'N/A' ? m.delta : m.delta + '%'} |` - ).join('\n'); - - const body = `\n` + - `## Binary Size Report\n\n` + - `| Metric | Baseline | PR | Change |\n` + - `|--------|----------|-----|--------|\n${rows}\n`; - - const threshold = 5; - const binaryDelta = metrics[0].delta; - if (binaryDelta !== 'N/A' && parseFloat(binaryDelta) > threshold) { - core.warning(`Binary size increased by ${binaryDelta}% (threshold: ${threshold}%)`); - } - - const comments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100, - } - ); - const existing = comments.find(c => - c.user?.type === 'Bot' && c.body?.includes('') - ); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index c8e870471392..8fcfa977afe9 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -31,27 +31,13 @@ permissions: id-token: write attestations: write actions: read + security-events: write jobs: changes: - name: Detect Changes - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - outputs: - should_build: ${{ steps.detect.outputs.any }} - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Detect changes - id: detect - uses: ./.github/actions/detect-changes - with: - platform: macos + uses: ./.github/workflows/_detect-changes.yml + with: + platform: macos build: needs: changes @@ -60,6 +46,7 @@ jobs: timeout-minutes: 120 strategy: + fail-fast: false matrix: build_type: ${{ inputs.build_type && fromJSON(format('["{0}"]', inputs.build_type)) || fromJSON('["Release"]') }} @@ -68,11 +55,6 @@ jobs: shell: bash steps: - - name: Harden Runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - name: Checkout repo uses: actions/checkout@v6 with: @@ -87,7 +69,6 @@ jobs: qt-host: mac qt-arch: clang_64 build-type: ${{ matrix.build_type }} - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 @@ -154,21 +135,13 @@ jobs: with: binary-path: /Volumes/QGroundControl/QGroundControl.app/Contents/MacOS/QGroundControl - - name: Attest Build with SBOM - if: matrix.build_type == 'Release' - uses: ./.github/actions/attest-sbom - with: - subject-path: ${{ runner.temp }}/build/QGroundControl.dmg - subject-name: QGroundControl - scan-path: ${{ runner.temp }}/build - - - name: Upload Build File + - name: Attest and Upload if: matrix.build_type == 'Release' - uses: ./.github/actions/upload + uses: ./.github/actions/attest-and-upload with: - artifact_name: QGroundControl.dmg - package_name: QGroundControl - aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} - aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} + artifact-name: QGroundControl.dmg + package-name: QGroundControl + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-distribution-id: ${{ secrets.AWS_DISTRIBUTION_ID }} diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 6d60e87e5ba3..2e0cb75441fb 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -31,27 +31,13 @@ permissions: id-token: write attestations: write actions: read + security-events: write jobs: changes: - name: Detect Changes - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - outputs: - should_build: ${{ steps.detect.outputs.any }} - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Detect changes - id: detect - uses: ./.github/actions/detect-changes - with: - platform: windows + uses: ./.github/workflows/_detect-changes.yml + with: + platform: windows build: name: Build ${{ matrix.arch }} ${{ matrix.build_type }} @@ -112,7 +98,6 @@ jobs: qt-arch: ${{ matrix.arch }} qt-version: ${{ matrix.qt_version || '' }} build-type: ${{ matrix.build_type }} - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Install GStreamer if: matrix.arch == 'win64_msvc2022_64' @@ -125,6 +110,17 @@ jobs: with: arch: ${{ (matrix.arch == 'win64_msvc2022_64' && 'x64') || (matrix.arch == 'win64_msvc2022_arm64' && 'arm64') || 'amd64_arm64' }} + - name: Install host Qt for ARM64 cross-compilation + if: matrix.arch == 'win64_msvc2022_arm64_cross_compiled' + id: qt-host + uses: ./.github/actions/qt-install + with: + version: ${{ steps.build-setup.outputs.qt_version }} + host: windows + arch: win64_msvc2022_64 + modules: ${{ steps.build-setup.outputs.qt_modules }} + export-env: 'false' + - name: Fix Rust linker resolution (ARM64 native) if: matrix.variant == 'arm64' shell: bash @@ -146,7 +142,7 @@ jobs: stable: ${{ (github.ref_type == 'tag' || contains(github.ref, 'Stable')) && 'true' || 'false' }} extra-args: >- -DQGC_ENABLE_GST_VIDEOSTREAMING=${{ matrix.arch == 'win64_msvc2022_64' && 'ON' || 'OFF' }} - ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && format('-DQT_HOST_PATH={0}/../msvc2022_64', env.QT_ROOT_DIR) || '' }} + ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && format('-DQT_HOST_PATH={0}', steps.qt-host.outputs.qt_root_dir) || '' }} - name: Build uses: ./.github/actions/cmake-build @@ -165,22 +161,14 @@ jobs: working-directory: ${{ runner.temp }}\build run: cmake --install . --config ${{ matrix.build_type }} - - name: Attest Build with SBOM - if: matrix.build_type == 'Release' - uses: ./.github/actions/attest-sbom - with: - subject-path: ${{ runner.temp }}/build/${{ matrix.package }}.exe - subject-name: ${{ matrix.package }} - scan-path: ${{ runner.temp }}/build - - - name: Upload artifact (installer) + - name: Attest and Upload if: matrix.build_type == 'Release' - uses: ./.github/actions/upload + uses: ./.github/actions/attest-and-upload with: - artifact_name: ${{ matrix.package }}.exe - package_name: ${{ matrix.package }} - aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} - aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} - upload_aws: ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && 'false' || 'true' }} + artifact-name: ${{ matrix.package }}.exe + package-name: ${{ matrix.package }} + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-distribution-id: ${{ secrets.AWS_DISTRIBUTION_ID }} + upload-aws: ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && 'false' || 'true' }} diff --git a/.gitignore b/.gitignore index feb50168cf3c..d8daa9afd380 100644 --- a/.gitignore +++ b/.gitignore @@ -26,9 +26,7 @@ tags # ------------------------------------------------------------------------------ # Build System & CMake # ------------------------------------------------------------------------------ -build*/ -!.github/actions/build-setup/ -!.github/actions/build-setup/action.yml +/build*/ cmake-build-*/ out/ CMakeFiles @@ -79,7 +77,6 @@ bin/mac *.dll # Build tool artifacts -Makefile *.ninja .ninja_log build.ninja diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8fab6eae708a..0cf210641680 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -67,7 +67,7 @@ repos: rev: v1.1.407 hooks: - id: pyright - files: ^(tools|test)/.*\.py$ + files: ^(tools|test|\.github/scripts)/.*\.py$ # Shell scripts - repo: https://github.com/shellcheck-py/shellcheck-py @@ -107,7 +107,6 @@ repos: rev: v1.37.1 hooks: - id: yamllint - args: ['-d', '{extends: default, rules: {line-length: {max: 200}, document-start: disable, truthy: {check-keys: false}, comments: {min-spaces-from-content: 1}}}'] # Markdown linting (report-only, doesn't auto-fix) - repo: https://github.com/igorshubovych/markdownlint-cli diff --git a/.qmlformat.ini b/.qmlformat.ini index 0ada5ddfb3d3..967e78da00c7 100644 --- a/.qmlformat.ini +++ b/.qmlformat.ini @@ -5,7 +5,7 @@ IndentWidth=4 UseTabs=false NormalizeOrder=true -NewlineType=native +NewlineType=unix ObjectsSpacing=true FunctionsSpacing=true SortImports=true diff --git a/.vscode/launch.default.json b/.vscode/launch.default.json index eed7731d54fc..e51a6123c663 100644 --- a/.vscode/launch.default.json +++ b/.vscode/launch.default.json @@ -17,18 +17,13 @@ "externalConsole": false, "name": "Launch QGroundControl", "preLaunchTask": "CMake: Build", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "launch", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "ignoreFailures": true, "text": "-enable-pretty-printing" - }, - { - "description": "Load Qt pretty printers", - "ignoreFailures": true, - "text": "source ${workspaceFolder}/tools/gdb-pretty-printers/qt6.py" } ], "stopAtEntry": false, @@ -43,7 +38,7 @@ }, "name": "Launch QGroundControl (LLDB)", "preLaunchTask": "CMake: Build", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "launch", "type": "lldb" }, @@ -57,7 +52,7 @@ "externalConsole": false, "name": "Run Unit Tests (Debug)", "preLaunchTask": "CMake: Build", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "launch", "setupCommands": [ { @@ -79,7 +74,7 @@ "externalConsole": false, "name": "Debug Specific Test", "preLaunchTask": "CMake: Build", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "launch", "setupCommands": [ { @@ -95,7 +90,7 @@ "MIMode": "gdb", "name": "Attach to QGroundControl", "processId": "${command:pickProcess}", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "attach", "setupCommands": [ { diff --git a/.vscode/tasks.default.json b/.vscode/tasks.default.json index 37a5c4636786..15b183330245 100644 --- a/.vscode/tasks.default.json +++ b/.vscode/tasks.default.json @@ -9,10 +9,14 @@ "tasks": [ { "args": [ - "--preset", - "default" + "-B", + "build", + "-G", + "Ninja", + "-DCMAKE_BUILD_TYPE=Debug", + "-DQGC_BUILD_TESTING=ON" ], - "command": "cmake", + "command": "qt-cmake", "group": "build", "label": "CMake: Configure (Debug)", "problemMatcher": [], @@ -20,10 +24,14 @@ }, { "args": [ - "--preset", - "default-release" + "-B", + "build", + "-G", + "Ninja", + "-DCMAKE_BUILD_TYPE=Release", + "-DQGC_BUILD_TESTING=OFF" ], - "command": "cmake", + "command": "qt-cmake", "group": "build", "label": "CMake: Configure (Release)", "problemMatcher": [], @@ -59,9 +67,11 @@ }, { "args": [ - "--unittest" + "--test-dir", + "build", + "--output-on-failure" ], - "command": "${workspaceFolder}/build/QGroundControl", + "command": "ctest", "dependsOn": "CMake: Build", "group": { "isDefault": true, @@ -73,9 +83,13 @@ }, { "args": [ - "--unittest:${input:testClass}" + "--test-dir", + "build", + "--output-on-failure", + "-R", + "${input:testClass}" ], - "command": "${workspaceFolder}/build/QGroundControl", + "command": "ctest", "dependsOn": "CMake: Build", "group": "test", "label": "Run Specific Test", @@ -84,33 +98,47 @@ }, { "args": [ - "--check" + "./tools/analyze.py", + "--tool", + "clang-format" ], - "command": "${workspaceFolder}/tools/format-check.sh", + "command": "python3", "group": "none", "label": "Format: Check Changed Files", "problemMatcher": [], "type": "shell" }, { - "args": [], - "command": "${workspaceFolder}/tools/format-check.sh", + "args": [ + "./tools/analyze.py", + "--tool", + "clang-format", + "--fix" + ], + "command": "python3", "group": "none", "label": "Format: Fix Changed Files", "problemMatcher": [], "type": "shell" }, { - "command": "${workspaceFolder}/tools/analyze.sh", + "args": [ + "./tools/analyze.py" + ], + "command": "python3", "group": "none", "label": "Static Analysis", "problemMatcher": "$gcc", "type": "shell" }, { - "command": "${workspaceFolder}/tools/clean.sh", + "args": [ + "run", + "--all-files" + ], + "command": "pre-commit", "group": "none", - "label": "Clean All", + "label": "Pre-commit: Run All", "problemMatcher": [], "type": "shell" }, diff --git a/.yamllint.yml b/.yamllint.yml index 448339bafae5..3283565553bc 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -2,8 +2,11 @@ extends: default rules: document-start: disable - line-length: disable + line-length: + max: 200 comments-indentation: disable truthy: allowed-values: ["true", "false"] check-keys: false + comments: + min-spaces-from-content: 1 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..71476374aa37 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,97 @@ +# AGENTS.md + +Instructions for AI coding agents (Codex, Claude Code, etc.) working on QGroundControl. + +## Quick References + +- [CODING_STYLE.md](CODING_STYLE.md) — Naming, formatting, C++20 features, QML style, logging +- [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) — Architecture patterns (Fact System, Multi-Vehicle, FirmwarePlugin) +- [.github/copilot-instructions.md](.github/copilot-instructions.md) — Code structure, CI structure, quick patterns +- [tools/README.md](tools/README.md) — Development scripts and tooling + +## Build & Test Commands + +```bash +# Configure (Release) +cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + +# Build +cmake --build build --config Release --parallel + +# Run unit tests +cd build && ctest --output-on-failure -L Unit --parallel $(nproc) + +# Lint (pre-commit hooks) +pre-commit run --all-files + +# Format C++ +clang-format -i path/to/changed/files.cc + +# CI Python script tests +cd .github/scripts && PYTHONPATH=. python3 -m pytest tests/ -q + +# Tools Python tests +cd tools && uv run --extra scripts --extra test pytest tests/ -q +``` + +## Golden Rules + +1. **Fact System**: ALL vehicle parameters use Facts. Never create custom parameter storage. +2. **Multi-Vehicle**: ALWAYS null-check `MultiVehicleManager::instance()->activeVehicle()` +3. **Firmware Plugin**: Use `vehicle->firmwarePlugin()` for firmware-specific behavior. +4. **QML Sizing**: Use `ScreenTools.defaultFontPixelHeight/Width`, never hardcoded values. +5. **QML Colors**: Use `QGCPalette`, never hardcoded colors. +6. **Match existing style**: Follow conventions of surrounding code. See CODING_STYLE.md. + +## Project Layout + +``` +src/ # C++/QML application source +├── Vehicle/ # Vehicle state and communications +├── FactSystem/ # Parameter management (Fact, FactGroup, FactMetaData) +├── FirmwarePlugin/ # PX4/ArduPilot abstraction +├── MissionManager/ # Mission planning +├── MAVLink/ # Protocol handling +├── QmlControls/ # Reusable QML components +└── Settings/ # Persistent settings + +tools/ # Development scripts and shared Python modules +├── common/ # Shared Python modules (gh_actions, build_config, etc.) +├── setup/ # Environment setup scripts +├── analyzers/ # Static analysis tools +└── pyproject.toml # Python dependencies (uv) + +.github/ +├── workflows/ # CI workflows (linux, macos, windows, android, ios) +├── actions/ # Composite actions (cmake-build, run-unit-tests, etc.) +├── scripts/ # CI-specific Python scripts +│ ├── common/ # Shared CI modules +│ └── templates/ # Jinja2 templates +└── build-config.json # Centralized version numbers +``` + +## CI Conventions + +- Platform workflows share logic via composite actions and reusable workflows. +- CI Python scripts use `httpx` for GitHub API and `jinja2` for templating. +- Bootstrap scripts (`install_dependencies.py`, `ccache_helper.py`) use stdlib only. +- Version numbers live in `.github/build-config.json`. +- Use `common.gh_actions.write_github_output()` for `$GITHUB_OUTPUT` writes. + +## C++ Key Patterns + +```cpp +// Always null-check vehicle +Vehicle* vehicle = MultiVehicleManager::instance()->activeVehicle(); +if (!vehicle) return; + +// Access parameters via Fact System +Fact* param = vehicle->parameterManager()->getParameter(-1, "PARAM_NAME"); +if (param) param->setCookedValue(newValue); +``` + +```qml +// QML vehicle access +property var vehicle: QGroundControl.multiVehicleManager.activeVehicle +enabled: vehicle && vehicle.armed +``` diff --git a/Makefile b/Makefile index f677f7c4f18d..d91993fdf59d 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ help: @echo " Qt version: $(QT_VERSION)" # Configuration - reads from centralized config -QT_VERSION := $(shell ./tools/setup/read-config.sh qt_version 2>/dev/null || echo "6.10.1") +QT_VERSION := $(shell python3 ./tools/setup/read_config.py --get qt_version 2>/dev/null || echo "6.10.1") QT_DIR ?= $(HOME)/Qt/$(QT_VERSION)/gcc_64 BUILD_TYPE ?= Debug BUILD_DIR := build @@ -51,7 +51,7 @@ submodules: deps: @echo "Installing dependencies (requires sudo)..." - sudo ./tools/setup/install-dependencies-debian.sh + python3 ./tools/setup/install_dependencies.py --platform debian # Build targets configure: submodules @@ -81,22 +81,22 @@ lint: pre-commit run --all-files format: - ./tools/analyze.sh --tool clang-format + python3 ./tools/analyze.py --tool clang-format format-fix: - ./tools/analyze.sh --tool clang-format --fix + python3 ./tools/analyze.py --tool clang-format --fix analyze: - ./tools/analyze.sh + python3 ./tools/analyze.py coverage: - ./tools/coverage.sh + python3 ./tools/coverage.py check: lint test # Other targets run: - ./$(BUILD_DIR)/staging/QGroundControl + ./$(BUILD_DIR)/$(BUILD_TYPE)/QGroundControl docs: npm run docs:build diff --git a/cmake/find-modules/FindQGCGStreamer.cmake b/cmake/find-modules/FindQGCGStreamer.cmake index 2a5b96a1c6f8..d267994b5b74 100644 --- a/cmake/find-modules/FindQGCGStreamer.cmake +++ b/cmake/find-modules/FindQGCGStreamer.cmake @@ -80,7 +80,7 @@ function(_qgc_validate_expanded_pkg EXPANDED_DIR LABEL) endif() endfunction() -if(WIN32) +if(WIN32 AND NOT ANDROID) if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") set(_gst_win_arch "arm64") else() @@ -130,7 +130,7 @@ if(WIN32) --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} ) -elseif(LINUX) +elseif(LINUX AND NOT ANDROID) if(NOT DEFINED GStreamer_ROOT_DIR) set(GStreamer_ROOT_DIR "/usr") endif() @@ -216,7 +216,7 @@ elseif(ANDROID) --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} ) -elseif(MACOS) +elseif(MACOS AND NOT IOS) if(NOT DEFINED GStreamer_ROOT_DIR) if(EXISTS "/Library/Frameworks/GStreamer.framework") set(GStreamer_ROOT_DIR "/Library/Frameworks/GStreamer.framework/Versions/1.0") diff --git a/justfile b/justfile index 4cda8e484f4f..d18c28cd9e04 100644 --- a/justfile +++ b/justfile @@ -92,7 +92,7 @@ check: lint test # Launch QGroundControl run: - ./{{build_dir}}/staging/QGroundControl + ./{{build_dir}}/{{build_type}}/QGroundControl # Build documentation docs: diff --git a/pyrightconfig.json b/pyrightconfig.json index fa8fa6295b4d..f59621a51177 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -6,9 +6,10 @@ ], "include": [ "tools/**/*.py", - "test/**/*.py" + "test/**/*.py", + ".github/scripts/**/*.py" ], - "pythonVersion": "3.10", + "pythonVersion": "3.12", "reportMissingImports": "warning", "reportMissingTypeStubs": false, "reportUnusedImport": "warning", diff --git a/ruff.toml b/ruff.toml index c417efa12b07..4a218d352410 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,7 +1,7 @@ # Ruff configuration for QGroundControl Python tools # https://docs.astral.sh/ruff/ -target-version = "py310" +target-version = "py312" line-length = 100 # Exclude build artifacts and external dependencies @@ -42,5 +42,5 @@ known-first-party = ["qgc"] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false -line-ending = "auto" +line-ending = "lf" docstring-code-format = true diff --git a/test/QmlControls/ObjectListModelBaseTest.cc b/test/QmlControls/ObjectListModelBaseTest.cc index 6eee012e1353..6d10ad25a9e8 100644 --- a/test/QmlControls/ObjectListModelBaseTest.cc +++ b/test/QmlControls/ObjectListModelBaseTest.cc @@ -137,5 +137,3 @@ void ObjectListModelBaseTest::_hasChildrenNonRootAlwaysFalse() } UT_REGISTER_TEST(ObjectListModelBaseTest, TestLabel::Unit) - -#include "ObjectListModelBaseTest.moc" diff --git a/test/UnitTestFramework/Tests/TestBaseClassesTest.h b/test/UnitTestFramework/Tests/TestBaseClassesTest.h index c3ecd7342cbf..2a8c876e93d9 100644 --- a/test/UnitTestFramework/Tests/TestBaseClassesTest.h +++ b/test/UnitTestFramework/Tests/TestBaseClassesTest.h @@ -30,12 +30,12 @@ class SimpleVehicleTest : public VehicleTest bool wasInitCalled() const { - return _initCalled; + return _initWasCalled; } bool wasCleanupCalled() const { - return _cleanupCalled; + return _cleanupWasCalled; } bool hadVehicle() const @@ -43,17 +43,14 @@ class SimpleVehicleTest : public VehicleTest return _hadVehicle; } - // Public wrappers for testing void doInit() { - UnitTest::initTestCase(); init(); } void doCleanup() { cleanup(); - UnitTest::cleanupTestCase(); } public slots: @@ -61,26 +58,19 @@ public slots: void init() override { VehicleTest::init(); - _initCalled = true; + _initWasCalled = true; _hadVehicle = (vehicle() != nullptr); } void cleanup() override { - _cleanupCalled = true; + _cleanupWasCalled = true; VehicleTest::cleanup(); } -private slots: - - void _dummyTest() - { - // Just a placeholder test method - } - private: - bool _initCalled = false; - bool _cleanupCalled = false; + bool _initWasCalled = false; + bool _cleanupWasCalled = false; bool _hadVehicle = false; }; @@ -98,17 +88,14 @@ class SimpleOfflineMissionTest : public OfflineMissionTest return missionController() != nullptr; } - // Public wrappers for testing void doInit() { - UnitTest::initTestCase(); init(); } void doCleanup() { cleanup(); - UnitTest::cleanupTestCase(); } public slots: @@ -122,12 +109,6 @@ public slots: { OfflineMissionTest::cleanup(); } - -private slots: - - void _dummyTest() - { - } }; /// Simple derived class to test CommsTest base @@ -144,17 +125,14 @@ class SimpleCommsTest : public CommsTest return linkManager() != nullptr; } - // Public wrappers for testing - expose protected methods void doInit() { - UnitTest::initTestCase(); init(); } void doCleanup() { cleanup(); - UnitTest::cleanupTestCase(); } SharedLinkInterfacePtr doCreateMockLink(const QString& name) @@ -178,10 +156,4 @@ public slots: { CommsTest::cleanup(); } - -private slots: - - void _dummyTest() - { - } }; diff --git a/tools/README.md b/tools/README.md index 8d38dc5526e3..d4c26961e46b 100644 --- a/tools/README.md +++ b/tools/README.md @@ -190,7 +190,7 @@ Scripts in `setup/` help configure development environments. They read configura | `install_dependencies.py --platform windows` | Windows | Install GStreamer (Vulkan SDK optional) | | `install_python.py` | All | Install Python tools via uv or pip | | `build-gstreamer.py` | All | Build GStreamer from source (optional) | -| `download_artifacts.py` | All | Download build artifacts from GitHub Actions | +| `download_artifacts.py` | All | Download build artifacts (in `.github/scripts/`) | | `read_config.py` | All | Read `.github/build-config.json` (Python, cross-platform) | ### Usage Examples @@ -285,7 +285,7 @@ Common utilities in `common/` are used by multiple tools: - `patterns.py` - QGC-specific regex patterns (Fact, FactGroup, MAVLink) - `file_traversal.py` - File discovery with proper filtering -- `gh_actions.py` - Shared `gh api` helpers for workflow runs and artifacts +- `gh_actions.py` - GitHub API helpers (httpx with `gh` CLI fallback) for workflow runs and artifacts See [common/README.md](common/README.md) for API documentation. diff --git a/tools/common/build_config.py b/tools/common/build_config.py index 312a91d60047..9af1553b61d6 100644 --- a/tools/common/build_config.py +++ b/tools/common/build_config.py @@ -30,6 +30,7 @@ "cmake_minimum_version", "macos_deployment_target", "ios_deployment_target", + "platform_workflows", ] IOS_QT_MODULE_EXCLUDES = {"qtserialport", "qtscxml"} diff --git a/tools/common/gh_actions.py b/tools/common/gh_actions.py index de3d434574fd..e4253addec68 100644 --- a/tools/common/gh_actions.py +++ b/tools/common/gh_actions.py @@ -8,9 +8,6 @@ import subprocess import time from typing import Any -from urllib import error as urllib_error -from urllib import parse as urllib_parse -from urllib import request as urllib_request def gh(*args: str, check: bool = True) -> subprocess.CompletedProcess: """Run a gh CLI command and return the process result.""" @@ -31,62 +28,65 @@ def _should_use_http_api() -> bool: return mode == "http" and bool(_github_token()) -def _extract_next_link(link_header: str) -> str: - for part in link_header.split(","): - section = part.strip() - if 'rel="next"' not in section: - continue - start = section.find("<") - end = section.find(">", start + 1) - if start != -1 and end != -1: - return section[start + 1:end] - return "" +def _build_http_client(): + import httpx + transport = httpx.HTTPTransport(retries=3) + base_url = os.environ.get("GITHUB_API_URL", "https://api.github.com").rstrip("/") + return httpx.Client( + base_url=base_url, + transport=transport, + timeout=30.0, + headers={ + "Accept": "application/vnd.github+json", + "User-Agent": "qgc-ci-gh-actions/1.0", + "X-GitHub-Api-Version": "2022-11-28", + "Authorization": f"Bearer {_github_token()}", + }, + ) + + +def _retry_after_seconds(value: str, fallback: float) -> float: + """Parse a Retry-After header value, falling back to a simple backoff.""" + try: + retry_after = float(value) + except (TypeError, ValueError): + return fallback + return retry_after if retry_after > 0 else fallback + +def _http_get_with_retries(client, url: str, **kwargs) -> Any: + """GET a GitHub API endpoint with retries for transient status failures.""" + import httpx -def _http_get_json(url: str, headers: dict[str, str], retries: int = 3) -> tuple[dict[str, Any], str]: - for attempt in range(1, retries + 1): - req = urllib_request.Request(url, headers=headers, method="GET") + retryable_statuses = {429, 500, 502, 503, 504} + + for attempt in range(1, 4): try: - with urllib_request.urlopen(req, timeout=30) as resp: - payload = resp.read().decode("utf-8") - doc = json.loads(payload) - if not isinstance(doc, dict): - raise ValueError(f"Unexpected non-object response from GitHub API at {url}") - next_url = _extract_next_link(resp.headers.get("Link", "")) - return doc, next_url - except urllib_error.HTTPError as exc: - if attempt < retries and exc.code in {429, 500, 502, 503, 504}: - time.sleep(attempt) - continue - raise RuntimeError(f"GitHub API request failed ({exc.code}) for {url}: {exc.reason}") from exc - except (urllib_error.URLError, TimeoutError, json.JSONDecodeError, ValueError) as exc: - if attempt < retries: - time.sleep(attempt) - continue - raise RuntimeError(f"GitHub API request failed for {url}: {exc}") from exc - - raise RuntimeError(f"GitHub API request failed for {url}: exhausted retries") + resp = client.get(url, **kwargs) + resp.raise_for_status() + return resp + except httpx.HTTPStatusError as exc: + status_code = exc.response.status_code + if attempt >= 3 or status_code not in retryable_statuses: + raise + delay = _retry_after_seconds(exc.response.headers.get("Retry-After", ""), float(attempt)) + except httpx.RequestError: + if attempt >= 3: + raise + delay = float(attempt) + + time.sleep(delay) def _http_paginated_docs(path: str, params: dict[str, str]) -> list[dict[str, Any]]: - base_url = os.environ.get("GITHUB_API_URL", "https://api.github.com").rstrip("/") - headers = { - "Accept": "application/vnd.github+json", - "User-Agent": "qgc-ci-gh-actions/1.0", - "X-GitHub-Api-Version": "2022-11-28", - "Authorization": f"Bearer {_github_token()}", - } - - query = urllib_parse.urlencode(params) - url = f"{base_url}/{path.lstrip('/')}" - if query: - url = f"{url}?{query}" - docs: list[dict[str, Any]] = [] - while url: - doc, next_url = _http_get_json(url, headers=headers) - docs.append(doc) - url = next_url + with _build_http_client() as client: + resp = _http_get_with_retries(client, f"/{path.lstrip('/')}", params=params) + docs.append(resp.json()) + + while "next" in resp.links: + resp = _http_get_with_retries(client, resp.links["next"]["url"]) + docs.append(resp.json()) return docs @@ -182,10 +182,72 @@ def list_run_artifacts(repo: str, run_id: int | str) -> list[dict[str, Any]]: return artifacts +def parse_csv_list(value: str) -> list[str]: + """Parse comma-separated values into a trimmed non-empty list.""" + return [item.strip() for item in value.split(",") if item.strip()] + + +def is_fork_pr() -> bool: + """Check if the current event is a PR from a fork repository.""" + event = os.environ.get("EVENT_NAME", os.environ.get("GITHUB_EVENT_NAME", "")) + if event != "pull_request": + return False + pr_repo = os.environ.get("PR_REPO", "").strip() + this_repo = os.environ.get("THIS_REPO", os.environ.get("GITHUB_REPOSITORY", "")).strip() + return bool(pr_repo and this_repo and pr_repo != this_repo) + + +def resolve_cache_policy(requested: str) -> str: + """Resolve cache save policy. + + Args: + requested: "auto", "true", or "false" + + Returns: + "true" or "false" + """ + if requested != "auto": + return requested + return "false" if is_fork_pr() else "true" + + def write_github_output(outputs: dict[str, str]) -> None: - """Write key=value pairs to $GITHUB_OUTPUT for GitHub Actions.""" + """Write key=value pairs to $GITHUB_OUTPUT for GitHub Actions. + + Handles multiline values using hash-based heredoc delimiters. + """ + import hashlib + github_output = os.environ.get('GITHUB_OUTPUT') - if github_output: - with open(github_output, 'a', encoding="utf-8") as f: - for key, value in outputs.items(): + if not github_output: + return + + with open(github_output, 'a', encoding="utf-8") as f: + for key, value in outputs.items(): + if "\n" in value: + value_hash = hashlib.sha256(value.encode("utf-8")).hexdigest()[:12] + delim = f"EOF_{key}_{value_hash}" + while delim in value: + delim = f"{delim}_X" + f.write(f"{key}<<{delim}\n{value}\n{delim}\n") + else: f.write(f"{key}={value}\n") + + +def write_step_summary(markdown: str) -> None: + """Append markdown content to $GITHUB_STEP_SUMMARY.""" + path = os.environ.get("GITHUB_STEP_SUMMARY") + if not path: + return + with open(path, "a", encoding="utf-8") as f: + f.write(markdown) + + +def append_github_env(values: dict[str, str]) -> None: + """Append environment variables to $GITHUB_ENV.""" + path = os.environ.get("GITHUB_ENV") + if not path: + return + with open(path, "a", encoding="utf-8") as f: + for key, value in values.items(): + f.write(f"{key}={value}\n") diff --git a/tools/pre_commit.py b/tools/pre_commit.py index b4a583609186..85b7c57a6208 100644 --- a/tools/pre_commit.py +++ b/tools/pre_commit.py @@ -15,6 +15,7 @@ ensure_tools_dir(__file__) +from common.gh_actions import write_github_output as _write_github_output, write_step_summary as _write_step_summary from common.logging import log_error, log_info, log_ok, log_warn HOOK_RESULT_RE = re.compile(r"\b(Passed|Failed|Skipped)\b") @@ -91,41 +92,31 @@ def run_command(cmd: list[str]) -> subprocess.CompletedProcess[str]: def write_github_output(exit_code: int, passed: int, failed: int, skipped: int, summary_lines: list[str]) -> None: - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - delimiter = f"PRECOMMIT_SUMMARY_{os.getpid()}" - with open(github_output, "a", encoding="utf-8") as handle: - handle.write(f"exit_code={exit_code}\n") - handle.write(f"passed={passed}\n") - handle.write(f"failed={failed}\n") - handle.write(f"skipped={skipped}\n") - handle.write(f"summary<<{delimiter}\n") - handle.write("\n".join(summary_lines) + "\n") - handle.write(f"{delimiter}\n") + _write_github_output({ + "exit_code": str(exit_code), + "passed": str(passed), + "failed": str(failed), + "skipped": str(skipped), + "summary": "\n".join(summary_lines), + }) def write_step_summary(exit_code: int, passed: int, failed: int, skipped: int, output: str) -> None: - step_summary = os.environ.get("GITHUB_STEP_SUMMARY") - if not step_summary: - return - with open(step_summary, "a", encoding="utf-8") as handle: - handle.write("## Pre-commit Results\n\n") - handle.write("**All checks passed**\n\n" if exit_code == 0 else "**Some checks failed**\n\n") - handle.write("| Status | Count |\n|--------|-------|\n") - handle.write(f"| Passed | {passed} |\n") - handle.write(f"| Failed | {failed} |\n") - if skipped > 0: - handle.write(f"| Skipped | {skipped} |\n") - handle.write("\n
    \nHook Results\n\n```\n") - handle.write("\n".join(extract_hook_lines(output)) + "\n") - handle.write("```\n
    \n") - - diff = subprocess.run(["git", "diff", "--stat"], capture_output=True, text=True, check=False) - if diff.stdout.strip(): - handle.write("\n
    \nFiles Modified by Hooks\n\n```\n") - handle.write(diff.stdout) - handle.write("```\n
    \n") + parts = ["## Pre-commit Results\n"] + parts.append("**All checks passed**\n" if exit_code == 0 else "**Some checks failed**\n") + parts.append("| Status | Count |\n|--------|-------|") + parts.append(f"| Passed | {passed} |") + parts.append(f"| Failed | {failed} |") + if skipped > 0: + parts.append(f"| Skipped | {skipped} |") + hook_lines = "\n".join(extract_hook_lines(output)) + parts.append(f"\n
    \nHook Results\n\n```\n{hook_lines}\n```\n
    ") + + diff = subprocess.run(["git", "diff", "--stat"], capture_output=True, text=True, check=False) + if diff.stdout.strip(): + parts.append(f"\n
    \nFiles Modified by Hooks\n\n```\n{diff.stdout}```\n
    ") + + _write_step_summary("\n".join(parts) + "\n") def handle_install() -> int: diff --git a/tools/pyproject.toml b/tools/pyproject.toml index 38ff5dac78bd..aa70bb12ca55 100644 --- a/tools/pyproject.toml +++ b/tools/pyproject.toml @@ -6,6 +6,8 @@ requires-python = ">=3.12" [project.optional-dependencies] scripts = [ "defusedxml>=0.7.1", + "httpx>=0.28", + "jinja2>=3.1", ] precommit = [ "pre-commit", diff --git a/tools/run_tests.py b/tools/run_tests.py index 4e5960e2d5a3..afca33ce8783 100755 --- a/tools/run_tests.py +++ b/tools/run_tests.py @@ -27,6 +27,7 @@ from typing import Sequence from common import find_repo_root, Logger +from common.gh_actions import write_github_output as _write_github_output @dataclass @@ -281,20 +282,15 @@ def _build_command(self, binary: Path, test_args: list[str]) -> tuple[list[str], def write_github_output(self, result: TestResult) -> None: """Write results to GITHUB_OUTPUT for CI integration.""" - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - - try: - with open(github_output, "a") as f: - f.write(f"exit_code={result.exit_code}\n") - f.write(f"passed={'true' if result.exit_code == 0 else 'false'}\n") - if result.xml_path: - f.write(f"output_file={result.xml_path}\n") - if result.log_path: - f.write(f"log_file={result.log_path}\n") - except OSError as e: - self.log.warn(f"Failed to write GITHUB_OUTPUT: {e}") + outputs: dict[str, str] = { + "exit_code": str(result.exit_code), + "passed": "true" if result.exit_code == 0 else "false", + } + if result.xml_path: + outputs["output_file"] = str(result.xml_path) + if result.log_path: + outputs["log_file"] = str(result.log_path) + _write_github_output(outputs) def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: diff --git a/tools/setup/install_qt.py b/tools/setup/install_qt.py new file mode 100644 index 000000000000..31f1af146d5d --- /dev/null +++ b/tools/setup/install_qt.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +Install Qt SDK using aqtinstall with architecture resolution. + +Wraps aqtinstall with QGC-specific arch-directory mapping, cache key +generation, and path resolution. Used by the qt-install GitHub Action. + +Usage: + python tools/setup/install_qt.py --version 6.8.3 --host linux --arch linux_gcc_64 + python tools/setup/install_qt.py --version 6.8.3 --host mac --arch clang_64 --modules "qtcharts qtlocation" + python tools/setup/install_qt.py cache-key --arch linux_gcc_64 --modules "qtcharts" + python tools/setup/install_qt.py resolve-arch --arch win64_msvc2022_64 +""" + +from __future__ import annotations + +import argparse +import hashlib +import os +import shutil +import subprocess +import sys +from pathlib import Path + +_tools_dir = str(Path(__file__).resolve().parent.parent) +if _tools_dir not in sys.path: + sys.path.insert(0, _tools_dir) + +from common.gh_actions import write_github_output + +# aqtinstall creates directories that differ from the arch parameter. +# This mapping resolves the actual on-disk directory name. +_ARCH_DIR_MAP: dict[str, str] = { + "win64_msvc2022_arm64_cross_compiled": "msvc2022_arm64", +} + +_ARCH_DIR_PREFIXES = [ + ("linux_", ""), + ("win64_", ""), +] + + +def resolve_arch_dir(arch: str) -> str: + """Map a Qt arch identifier to the on-disk directory name aqtinstall creates.""" + if arch in _ARCH_DIR_MAP: + return _ARCH_DIR_MAP[arch] + for prefix, replacement in _ARCH_DIR_PREFIXES: + if arch.startswith(prefix): + return replacement + arch[len(prefix):] + if arch == "clang_64": + return "macos" + return arch + + +def compute_cache_digest(modules: str, archives: str) -> str: + """Generate a SHA-256 digest for cache key differentiation.""" + content = f"{modules}\n{archives}" + return hashlib.sha256(content.encode()).hexdigest() + + +def resolve_qt_root(outdir: Path, version: str, arch_dir: str) -> Path: + """Resolve and validate the Qt root directory after installation.""" + qt_root = outdir / version / arch_dir + if not qt_root.is_dir(): + available = "none" + version_dir = outdir / version + if version_dir.is_dir(): + available = ", ".join(sorted(p.name for p in version_dir.iterdir() if p.is_dir())) or "none" + print(f"::error::Qt root not found at {qt_root}") + print(f"Expected arch_dir '{arch_dir}' from arch, available: {available}") + sys.exit(1) + return qt_root + + +def install_qt( + host: str, + target: str, + version: str, + arch: str, + outdir: Path, + modules: str = "", + archives: str = "", +) -> Path: + """Install Qt using aqtinstall and return the resolved root directory.""" + aqt = shutil.which("aqt") + if not aqt: + subprocess.run( + [sys.executable, "-m", "pip", "install", "aqtinstall", "--quiet"], + check=True, + ) + aqt = shutil.which("aqt") + if not aqt: + print("::error::aqtinstall not found after pip install") + sys.exit(1) + + args = [aqt, "install-qt", host, target, version, arch, "--outputdir", str(outdir)] + + if modules: + args.extend(["--modules", *modules.split()]) + if archives: + args.extend(["--archives", *archives.split()]) + + print(f"Running: {' '.join(args)}") + subprocess.run(args, check=True) + + arch_dir = resolve_arch_dir(arch) + return resolve_qt_root(outdir, version, arch_dir) + + +# Android ABI → aqtinstall arch mapping (priority order for resolution) +_ANDROID_ABI_ORDER = [ + ("arm64-v8a", "arm64"), + ("armeabi-v7a", "armv7"), + ("x86_64", "x86_64"), + ("x86", "x86"), +] + + +def resolve_android_qt_root(abis: str, roots: dict[str, str]) -> str: + """Pick the primary Android Qt root from installed ABIs. + + Args: + abis: Semicolon-separated ABI list (e.g. "arm64-v8a;x86_64") + roots: Mapping of short ABI key → Qt root path (e.g. {"arm64": "/path/to/qt"}) + + Returns: + The Qt root path for the highest-priority installed ABI. + + Raises: + SystemExit: If no matching ABI is found. + """ + abi_set = {a.strip() for a in abis.split(";") if a.strip()} + for abi, key in _ANDROID_ABI_ORDER: + if abi in abi_set and roots.get(key): + return roots[key] + print(f"::error::Failed to resolve an installed Android Qt root for ABIs: {abis}") + sys.exit(1) + + +def _add_arch_args(p: argparse.ArgumentParser) -> None: + p.add_argument("--arch", required=True) + p.add_argument("--modules", default="") + p.add_argument("--archives", default="") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Install Qt SDK using aqtinstall.") + sub = parser.add_subparsers(dest="command", required=True) + + install_p = sub.add_parser("install", help="Install Qt") + install_p.add_argument("--version", required=True) + install_p.add_argument("--host", default="linux") + install_p.add_argument("--target", default="desktop") + install_p.add_argument("--outdir", type=Path, default=Path(".qt")) + _add_arch_args(install_p) + + cache_p = sub.add_parser("cache-key", help="Output arch_dir and cache digest") + _add_arch_args(cache_p) + + resolve_p = sub.add_parser("resolve-arch", help="Print resolved arch directory name") + resolve_p.add_argument("--arch", required=True) + + android_p = sub.add_parser("resolve-android-root", help="Pick primary Android Qt root from installed ABIs") + android_p.add_argument("--abis", required=True, help="Semicolon-separated ABI list") + android_p.add_argument("--arm64", default="") + android_p.add_argument("--armv7", default="") + android_p.add_argument("--x86-64", default="", dest="x86_64") + android_p.add_argument("--x86", default="") + + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + + if args.command == "resolve-arch": + print(resolve_arch_dir(args.arch)) + return 0 + + if args.command == "resolve-android-root": + roots = {"arm64": args.arm64, "armv7": args.armv7, "x86_64": args.x86_64, "x86": args.x86} + qt_root = resolve_android_qt_root(args.abis, roots) + write_github_output({"qt_root_dir": qt_root}) + print(f"qt_root_dir={qt_root}") + return 0 + + if args.command == "cache-key": + arch_dir = resolve_arch_dir(args.arch) + digest = compute_cache_digest(args.modules, args.archives) + write_github_output({"arch_dir": arch_dir, "digest": digest}) + print(f"arch_dir={arch_dir}") + print(f"digest={digest}") + return 0 + + # Default: install + arch_dir = resolve_arch_dir(args.arch) + qt_root = install_qt( + host=args.host, + target=args.target, + version=args.version, + arch=args.arch, + outdir=args.outdir, + modules=args.modules, + archives=args.archives, + ) + + write_github_output({ + "arch_dir": arch_dir, + "qt_root_dir": str(qt_root), + "qt_bin_dir": str(qt_root / "bin"), + }) + print(f"Qt installed at {qt_root}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/conftest.py b/tools/tests/conftest.py new file mode 100644 index 000000000000..7bb0b2fa7e98 --- /dev/null +++ b/tools/tests/conftest.py @@ -0,0 +1,11 @@ +"""Shared pytest setup for tools tests.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +# Ensure the tools directory is importable (for common.*, setup.*, etc.) +tools_dir = str(Path(__file__).resolve().parent.parent) +if tools_dir not in sys.path: + sys.path.insert(0, tools_dir) diff --git a/tools/tests/test_gh_actions.py b/tools/tests/test_gh_actions.py index b66b974f5f7a..aff63550f98a 100644 --- a/tools/tests/test_gh_actions.py +++ b/tools/tests/test_gh_actions.py @@ -8,6 +8,8 @@ import subprocess from unittest.mock import patch +import httpx + from common import gh_actions as mod @@ -15,19 +17,20 @@ def _cp(stdout: str = "", stderr: str = "", returncode: int = 0) -> subprocess.C return subprocess.CompletedProcess(args=["gh"], returncode=returncode, stdout=stdout, stderr=stderr) -class _FakeHttpResponse: - def __init__(self, payload: dict[str, object], link: str = "") -> None: - self._payload = payload - self.headers = {"Link": link} - - def read(self) -> bytes: - return json.dumps(self._payload).encode("utf-8") - - def __enter__(self) -> _FakeHttpResponse: - return self - - def __exit__(self, exc_type, exc, tb) -> None: - return None +def _mock_response( + data: dict, + next_url: str = "", + status_code: int = 200, + extra_headers: dict[str, str] | None = None, +) -> httpx.Response: + headers = {} + if next_url: + headers["link"] = f'<{next_url}>; rel="next"' + if extra_headers: + headers.update(extra_headers) + request = httpx.Request("GET", "https://api.github.com/test") + resp = httpx.Response(status_code, json=data, headers=headers, request=request) + return resp def test_parse_json_documents_handles_paginated_stream() -> None: @@ -63,19 +66,43 @@ def test_list_workflow_runs_for_sha_uses_get_method() -> None: def test_list_workflow_runs_for_sha_http_mode() -> None: - first = _FakeHttpResponse( + first = _mock_response( {"workflow_runs": [{"id": 1, "name": "Linux"}]}, - '; rel="next"', + next_url="https://api.github.com/next", ) - second = _FakeHttpResponse({"workflow_runs": [{"id": 2, "name": "Windows"}]}) + second = _mock_response({"workflow_runs": [{"id": 2, "name": "Windows"}]}) + with patch.dict(os.environ, {"QGC_GH_API_MODE": "http", "GH_TOKEN": "token"}, clear=False): - with patch.object(mod.urllib_request, "urlopen", side_effect=[first, second]), patch.object(mod, "gh") as gh_mock: - runs = mod.list_workflow_runs_for_sha("owner/repo", "abc123") + with patch.object(mod, "_build_http_client") as mock_client: + client = mock_client.return_value.__enter__.return_value + client.get.side_effect = [first, second] + with patch.object(mod, "gh") as gh_mock: + runs = mod.list_workflow_runs_for_sha("owner/repo", "abc123") assert [run["id"] for run in runs] == [1, 2] gh_mock.assert_not_called() +def test_list_workflow_runs_for_sha_http_mode_retries_retryable_status() -> None: + first = _mock_response( + {"message": "try again"}, + status_code=503, + extra_headers={"Retry-After": "0"}, + ) + second = _mock_response({"workflow_runs": [{"id": 7, "name": "Linux"}]}) + + with patch.dict(os.environ, {"QGC_GH_API_MODE": "http", "GH_TOKEN": "token"}, clear=False): + with patch.object(mod, "_build_http_client") as mock_client: + client = mock_client.return_value.__enter__.return_value + client.get.side_effect = [first, second] + with patch.object(mod.time, "sleep") as sleep_mock: + runs = mod.list_workflow_runs_for_sha("owner/repo", "abc123") + + assert runs == [{"id": 7, "name": "Linux"}] + assert client.get.call_count == 2 + sleep_mock.assert_called_once_with(1.0) + + def test_list_run_artifacts_parses_paginated_stream() -> None: payload = ( json.dumps({"artifacts": [{"name": "QGroundControl", "size_in_bytes": 1}]}) @@ -96,3 +123,98 @@ def test_list_run_artifacts_rejects_invalid_run_id() -> None: assert "run_id must be an integer" in str(exc) else: raise AssertionError("Expected ValueError for invalid run_id") + + +class TestIsForkPr: + def test_not_pr_event(self) -> None: + with patch.dict(os.environ, {"EVENT_NAME": "push"}, clear=False): + assert mod.is_fork_pr() is False + + def test_same_repo_pr(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "owner/repo", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.is_fork_pr() is False + + def test_fork_pr(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "fork/repo", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.is_fork_pr() is True + + def test_empty_pr_repo(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.is_fork_pr() is False + + +class TestResolveCachePolicy: + def test_explicit_true(self) -> None: + assert mod.resolve_cache_policy("true") == "true" + + def test_explicit_false(self) -> None: + assert mod.resolve_cache_policy("false") == "false" + + def test_auto_non_pr(self) -> None: + with patch.dict(os.environ, {"EVENT_NAME": "push"}, clear=False): + assert mod.resolve_cache_policy("auto") == "true" + + def test_auto_same_repo_pr(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "owner/repo", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.resolve_cache_policy("auto") == "true" + + def test_auto_fork_pr(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "fork/repo", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.resolve_cache_policy("auto") == "false" + + +class TestWriteGithubOutput: + def test_simple_values(self, tmp_path) -> None: + out = tmp_path / "output" + out.touch() + with patch.dict(os.environ, {"GITHUB_OUTPUT": str(out)}, clear=False): + mod.write_github_output({"key1": "val1", "key2": "val2"}) + content = out.read_text() + assert "key1=val1\n" in content + assert "key2=val2\n" in content + + def test_multiline_value(self, tmp_path) -> None: + out = tmp_path / "output" + out.touch() + with patch.dict(os.environ, {"GITHUB_OUTPUT": str(out)}, clear=False): + mod.write_github_output({"body": "line1\nline2"}) + content = out.read_text() + assert "body< None: + with patch.dict(os.environ, {}, clear=True): + mod.write_github_output({"key": "val"}) + + +class TestWriteStepSummary: + def test_writes_markdown(self, tmp_path) -> None: + out = tmp_path / "summary" + out.touch() + with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(out)}, clear=False): + mod.write_step_summary("## Hello\n") + assert out.read_text() == "## Hello\n" + + def test_noop_without_env(self) -> None: + with patch.dict(os.environ, {}, clear=True): + mod.write_step_summary("test") + + +class TestAppendGithubEnv: + def test_writes_env_vars(self, tmp_path) -> None: + out = tmp_path / "env" + out.touch() + with patch.dict(os.environ, {"GITHUB_ENV": str(out)}, clear=False): + mod.append_github_env({"FOO": "bar", "BAZ": "qux"}) + content = out.read_text() + assert "FOO=bar\n" in content + assert "BAZ=qux\n" in content + + def test_noop_without_env(self) -> None: + with patch.dict(os.environ, {}, clear=True): + mod.append_github_env({"FOO": "bar"}) diff --git a/tools/tests/test_install_qt.py b/tools/tests/test_install_qt.py new file mode 100644 index 000000000000..3b64cf7ffa22 --- /dev/null +++ b/tools/tests/test_install_qt.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Tests for tools/setup/install_qt.py.""" + +from __future__ import annotations + +import pytest + +from setup.install_qt import compute_cache_digest, resolve_android_qt_root, resolve_arch_dir, resolve_qt_root + + +class TestResolveArchDir: + def test_linux_gcc_64(self) -> None: + assert resolve_arch_dir("linux_gcc_64") == "gcc_64" + + def test_linux_arm64(self) -> None: + assert resolve_arch_dir("linux_arm64") == "arm64" + + def test_win64_msvc2022_64(self) -> None: + assert resolve_arch_dir("win64_msvc2022_64") == "msvc2022_64" + + def test_win64_msvc2022_arm64_cross_compiled(self) -> None: + assert resolve_arch_dir("win64_msvc2022_arm64_cross_compiled") == "msvc2022_arm64" + + def test_clang_64_maps_to_macos(self) -> None: + assert resolve_arch_dir("clang_64") == "macos" + + def test_android_arm64_v8a_unchanged(self) -> None: + assert resolve_arch_dir("android_arm64_v8a") == "android_arm64_v8a" + + def test_ios_unchanged(self) -> None: + assert resolve_arch_dir("ios") == "ios" + + +class TestComputeCacheDigest: + def test_deterministic(self) -> None: + a = compute_cache_digest("qtcharts qtlocation", "") + b = compute_cache_digest("qtcharts qtlocation", "") + assert a == b + + def test_different_modules_differ(self) -> None: + a = compute_cache_digest("qtcharts", "") + b = compute_cache_digest("qtlocation", "") + assert a != b + + def test_archives_affect_digest(self) -> None: + a = compute_cache_digest("qtcharts", "") + b = compute_cache_digest("qtcharts", "icu") + assert a != b + + def test_returns_hex_string(self) -> None: + d = compute_cache_digest("", "") + assert len(d) == 64 + assert all(c in "0123456789abcdef" for c in d) + + +class TestResolveQtRoot: + def test_valid_path(self, tmp_path: "pytest.TempPathFactory") -> None: + qt_root = tmp_path / "6.8.3" / "gcc_64" + qt_root.mkdir(parents=True) + result = resolve_qt_root(tmp_path, "6.8.3", "gcc_64") + assert result == qt_root + + def test_missing_path_exits(self, tmp_path: "pytest.TempPathFactory") -> None: + with pytest.raises(SystemExit): + resolve_qt_root(tmp_path, "6.8.3", "gcc_64") + + +class TestResolveAndroidQtRoot: + def test_arm64_preferred(self) -> None: + roots = {"arm64": "/qt/arm64", "armv7": "/qt/armv7"} + assert resolve_android_qt_root("arm64-v8a;armeabi-v7a", roots) == "/qt/arm64" + + def test_armv7_fallback(self) -> None: + roots = {"arm64": "", "armv7": "/qt/armv7"} + assert resolve_android_qt_root("arm64-v8a;armeabi-v7a", roots) == "/qt/armv7" + + def test_x86_64_only(self) -> None: + roots = {"x86_64": "/qt/x86_64"} + assert resolve_android_qt_root("x86_64", roots) == "/qt/x86_64" + + def test_x86_only(self) -> None: + roots = {"x86": "/qt/x86"} + assert resolve_android_qt_root("x86", roots) == "/qt/x86" + + def test_no_match_exits(self) -> None: + with pytest.raises(SystemExit): + resolve_android_qt_root("mips", {}) + + def test_empty_root_skipped(self) -> None: + roots = {"arm64": "", "x86_64": "/qt/x86_64"} + assert resolve_android_qt_root("arm64-v8a;x86_64", roots) == "/qt/x86_64" + + def test_semicolon_parsing(self) -> None: + roots = {"armv7": "/qt/armv7"} + assert resolve_android_qt_root("armeabi-v7a", roots) == "/qt/armv7" diff --git a/tools/uv.lock b/tools/uv.lock index eddafc51fd85..f869054d19d7 100644 --- a/tools/uv.lock +++ b/tools/uv.lock @@ -6,6 +6,19 @@ resolution-markers = [ "python_full_version < '3.13'", ] +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + [[package]] name = "aqtinstall" version = "3.3.0" @@ -454,6 +467,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/be/f722c843e7875c7cf92cf0e0c1604cddda55a70278c768c6327a78fdba79/gcovr-8.6-py3-none-any.whl", hash = "sha256:dbf9d87c38042752ad6f530aa8210427e22b526611bb7b7bfed0e81977d1f1ef", size = 254618, upload-time = "2026-01-13T20:04:28.15Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "humanize" version = "4.15.0" @@ -1153,6 +1203,8 @@ qt = [ ] scripts = [ { name = "defusedxml" }, + { name = "httpx" }, + { name = "jinja2" }, ] test = [ { name = "jinja2" }, @@ -1165,7 +1217,9 @@ requires-dist = [ { name = "aqtinstall", marker = "extra == 'qt'" }, { name = "defusedxml", marker = "extra == 'scripts'", specifier = ">=0.7.1" }, { name = "gcovr", marker = "extra == 'coverage'" }, + { name = "httpx", marker = "extra == 'scripts'", specifier = ">=0.28" }, { name = "jinja2", marker = "extra == 'dev'" }, + { name = "jinja2", marker = "extra == 'scripts'", specifier = ">=3.1" }, { name = "jinja2", marker = "extra == 'test'" }, { name = "lsprotocol", marker = "extra == 'lsp'" }, { name = "meson", marker = "extra == 'ci'" }, From 238a21785b7b31bf6e3110ec60ac4118892f1490 Mon Sep 17 00:00:00 2001 From: Mehmet Date: Thu, 12 Mar 2026 21:05:51 +0300 Subject: [PATCH 080/119] =?UTF-8?q?Fix=20BatteryIndicator=20QML=20resource?= =?UTF-8?q?=20path=20(Controls=20=E2=86=92=20Toolbar)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/FirmwarePlugin/FirmwarePlugin.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FirmwarePlugin/FirmwarePlugin.cc b/src/FirmwarePlugin/FirmwarePlugin.cc index 813214343f2f..b4dfe8a795f1 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.cc +++ b/src/FirmwarePlugin/FirmwarePlugin.cc @@ -198,7 +198,7 @@ const QVariantList &FirmwarePlugin::toolIndicators(const Vehicle*) QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/GPSResilienceIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/TelemetryRSSIIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/RCRSSIIndicator.qml")), - QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Controls/BatteryIndicator.qml")), + QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/BatteryIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/RemoteIDIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/GimbalIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/EscIndicator.qml")), From a722dbb6ac23104124eb2c93737c90da2512acc1 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 12 Mar 2026 14:10:54 -0700 Subject: [PATCH 081/119] APM: Add camera capture commands to supported mission commands Add MAV_CMD_IMAGE_START_CAPTURE, MAV_CMD_IMAGE_STOP_CAPTURE, MAV_CMD_VIDEO_START_CAPTURE, and MAV_CMD_VIDEO_STOP_CAPTURE to APMFirmwarePlugin::supportedMissionCommands(). These commands are handled by ArduPilot's AP_Camera backend but were missing from QGC's ArduPilot mission command list, preventing users from adding them in Plan view. Also hide the Video Status Frequency parameter (param2) for MAV_CMD_VIDEO_START_CAPTURE in the APM command override JSON, since ArduPilot's MAVLink Camera V2 backend hardcodes this value to 0. MAV_CMD_SET_CAMERA_MODE is intentionally not added as ArduPilot does not implement it. --- src/FirmwarePlugin/APM/APM-MavCmdInfoCommon.json | 5 +++++ src/FirmwarePlugin/APM/APMFirmwarePlugin.cc | 1 + 2 files changed, 6 insertions(+) diff --git a/src/FirmwarePlugin/APM/APM-MavCmdInfoCommon.json b/src/FirmwarePlugin/APM/APM-MavCmdInfoCommon.json index b106da0495ff..6176998d4f4a 100644 --- a/src/FirmwarePlugin/APM/APM-MavCmdInfoCommon.json +++ b/src/FirmwarePlugin/APM/APM-MavCmdInfoCommon.json @@ -90,6 +90,11 @@ "enumValues": "1,0", "default": 1 } + }, + { + "id": 2500, + "comment": "MAV_CMD_VIDEO_START_CAPTURE", + "paramRemove": "2" } ] } diff --git a/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc b/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc index 7e521c67b859..fccea19e79a2 100644 --- a/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc @@ -555,6 +555,7 @@ QList APMFirmwarePlugin::supportedMissionCommands(QGCMAVLink::VehicleCl MAV_CMD_DO_MOUNT_CONTROL, MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW, MAV_CMD_DO_SET_CAM_TRIGG_DIST, + MAV_CMD_IMAGE_START_CAPTURE, MAV_CMD_IMAGE_STOP_CAPTURE, MAV_CMD_VIDEO_START_CAPTURE, MAV_CMD_VIDEO_STOP_CAPTURE, MAV_CMD_DO_FENCE_ENABLE, MAV_CMD_DO_PARACHUTE, MAV_CMD_DO_INVERTED_FLIGHT, From f9bad01ba5f832f9d566696b3e3fff592f4e32e6 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 12 Mar 2026 14:45:33 -0700 Subject: [PATCH 082/119] Rename MavlinkCameraControl to MavlinkCameraControlInterface Rename the base camera control class from MavlinkCameraControl to MavlinkCameraControlInterface to better reflect its role as an abstract interface. Updates all file names and references across 17 files. --- src/Camera/CMakeLists.txt | 4 +- ...ol.cc => MavlinkCameraControlInterface.cc} | 18 +++--- ...trol.h => MavlinkCameraControlInterface.h} | 6 +- src/Camera/QGCCameraIO.cc | 4 +- src/Camera/QGCCameraIO.h | 6 +- src/Camera/QGCCameraManager.cc | 56 +++++++++---------- src/Camera/QGCCameraManager.h | 12 ++-- src/Camera/SimulatedCameraControl.cc | 8 +-- src/Camera/SimulatedCameraControl.h | 4 +- src/Camera/VehicleCameraControl.cc | 6 +- src/Camera/VehicleCameraControl.h | 4 +- src/FirmwarePlugin/FirmwarePlugin.cc | 2 +- src/FirmwarePlugin/FirmwarePlugin.h | 4 +- src/FlightMap/Widgets/PhotoVideoControl.qml | 32 +++++------ src/FlyView/FlightDisplayViewVideo.qml | 8 +-- src/VideoManager/VideoManager.cc | 6 +- test/Camera/VehicleCameraControlTest.cc | 6 +- 17 files changed, 93 insertions(+), 93 deletions(-) rename src/Camera/{MavlinkCameraControl.cc => MavlinkCameraControlInterface.cc} (77%) rename src/Camera/{MavlinkCameraControl.h => MavlinkCameraControlInterface.h} (98%) diff --git a/src/Camera/CMakeLists.txt b/src/Camera/CMakeLists.txt index 8e973259bf1f..dc088982d619 100644 --- a/src/Camera/CMakeLists.txt +++ b/src/Camera/CMakeLists.txt @@ -7,8 +7,8 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE CameraMetaData.cc CameraMetaData.h - MavlinkCameraControl.cc - MavlinkCameraControl.h + MavlinkCameraControlInterface.cc + MavlinkCameraControlInterface.h QGCCameraIO.cc QGCCameraIO.h QGCCameraManager.cc diff --git a/src/Camera/MavlinkCameraControl.cc b/src/Camera/MavlinkCameraControlInterface.cc similarity index 77% rename from src/Camera/MavlinkCameraControl.cc rename to src/Camera/MavlinkCameraControlInterface.cc index 8624997c0a6e..44d1a9ec7cc4 100644 --- a/src/Camera/MavlinkCameraControl.cc +++ b/src/Camera/MavlinkCameraControlInterface.cc @@ -1,22 +1,22 @@ -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "QGCLoggingCategory.h" -QGC_LOGGING_CATEGORY(CameraControlLog, "Camera.MavlinkCameraControl") -QGC_LOGGING_CATEGORY(CameraControlVerboseLog, "Camera.MavlinkCameraControl:verbose") +QGC_LOGGING_CATEGORY(CameraControlLog, "Camera.MavlinkCameraControlInterface") +QGC_LOGGING_CATEGORY(CameraControlVerboseLog, "Camera.MavlinkCameraControlInterface:verbose") -MavlinkCameraControl::MavlinkCameraControl(Vehicle *vehicle, QObject *parent) +MavlinkCameraControlInterface::MavlinkCameraControlInterface(Vehicle *vehicle, QObject *parent) : FactGroup(0, parent, true /* ignore camel case */) , _vehicle(vehicle) { qCDebug(CameraControlLog) << this; } -MavlinkCameraControl::~MavlinkCameraControl() +MavlinkCameraControlInterface::~MavlinkCameraControlInterface() { qCDebug(CameraControlLog) << this; } -QString MavlinkCameraControl::captureImageStatusToStr(uint8_t image_status) +QString MavlinkCameraControlInterface::captureImageStatusToStr(uint8_t image_status) { switch (image_status) { case PHOTO_CAPTURE_IDLE: @@ -32,7 +32,7 @@ QString MavlinkCameraControl::captureImageStatusToStr(uint8_t image_status) } } -QString MavlinkCameraControl::captureVideoStatusToStr(uint8_t video_status) +QString MavlinkCameraControlInterface::captureVideoStatusToStr(uint8_t video_status) { switch (video_status) { case VIDEO_CAPTURE_STATUS_STOPPED: @@ -44,7 +44,7 @@ QString MavlinkCameraControl::captureVideoStatusToStr(uint8_t video_status) } } -QString MavlinkCameraControl::storageStatusToStr(uint8_t status) +QString MavlinkCameraControlInterface::storageStatusToStr(uint8_t status) { switch (status) { case STORAGE_STATUS_EMPTY: @@ -60,7 +60,7 @@ QString MavlinkCameraControl::storageStatusToStr(uint8_t status) } } -QString MavlinkCameraControl::cameraModeToStr(CameraMode mode) +QString MavlinkCameraControlInterface::cameraModeToStr(CameraMode mode) { switch (mode) { case CAM_MODE_UNDEFINED: diff --git a/src/Camera/MavlinkCameraControl.h b/src/Camera/MavlinkCameraControlInterface.h similarity index 98% rename from src/Camera/MavlinkCameraControl.h rename to src/Camera/MavlinkCameraControlInterface.h index 3fd9be8712e9..608c14a6e86a 100644 --- a/src/Camera/MavlinkCameraControl.h +++ b/src/Camera/MavlinkCameraControlInterface.h @@ -19,7 +19,7 @@ Q_DECLARE_LOGGING_CATEGORY(CameraControlLog) Q_DECLARE_LOGGING_CATEGORY(CameraControlVerboseLog) /// Abstract base class for all camera controls: real and simulated -class MavlinkCameraControl : public FactGroup +class MavlinkCameraControlInterface : public FactGroup { Q_OBJECT QML_ELEMENT @@ -93,8 +93,8 @@ class MavlinkCameraControl : public FactGroup friend class QGCCameraParamIO; public: - explicit MavlinkCameraControl(Vehicle *vehicle, QObject *parent = nullptr); - virtual ~MavlinkCameraControl(); + explicit MavlinkCameraControlInterface(Vehicle *vehicle, QObject *parent = nullptr); + virtual ~MavlinkCameraControlInterface(); enum CameraMode { CAM_MODE_UNDEFINED = -1, diff --git a/src/Camera/QGCCameraIO.cc b/src/Camera/QGCCameraIO.cc index cda1587524cb..7339d99c5a6e 100644 --- a/src/Camera/QGCCameraIO.cc +++ b/src/Camera/QGCCameraIO.cc @@ -1,5 +1,5 @@ #include "QGCCameraIO.h" -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "LinkInterface.h" #include "MAVLinkProtocol.h" #include "QGCLoggingCategory.h" @@ -12,7 +12,7 @@ namespace { constexpr int kMaxRetries = 3; } -QGCCameraParamIO::QGCCameraParamIO(MavlinkCameraControl *control, Fact *fact, Vehicle *vehicle) +QGCCameraParamIO::QGCCameraParamIO(MavlinkCameraControlInterface *control, Fact *fact, Vehicle *vehicle) : QObject(control) , _control(control) , _fact(fact) diff --git a/src/Camera/QGCCameraIO.h b/src/Camera/QGCCameraIO.h index 75a1f9d68340..3bcb0e74b22a 100644 --- a/src/Camera/QGCCameraIO.h +++ b/src/Camera/QGCCameraIO.h @@ -5,7 +5,7 @@ #include "MAVLinkLib.h" -class MavlinkCameraControl; +class MavlinkCameraControlInterface; class Fact; class Vehicle; @@ -16,7 +16,7 @@ Q_DECLARE_LOGGING_CATEGORY(CameraIOLogVerbose) class QGCCameraParamIO : public QObject { public: - QGCCameraParamIO(MavlinkCameraControl *control, Fact *fact, Vehicle *vehicle); + QGCCameraParamIO(MavlinkCameraControlInterface *control, Fact *fact, Vehicle *vehicle); ~QGCCameraParamIO(); void handleParamAck(const mavlink_param_ext_ack_t &ack); @@ -36,7 +36,7 @@ private slots: void _sendParameter(); QVariant _valueFromMessage(const char *value, uint8_t param_type); - MavlinkCameraControl *_control = nullptr; + MavlinkCameraControlInterface *_control = nullptr; Fact *_fact = nullptr; Vehicle *_vehicle = nullptr; diff --git a/src/Camera/QGCCameraManager.cc b/src/Camera/QGCCameraManager.cc index b6ad8c5b80a5..d9d407ac7eb8 100644 --- a/src/Camera/QGCCameraManager.cc +++ b/src/Camera/QGCCameraManager.cc @@ -3,7 +3,7 @@ #include "FirmwarePlugin.h" #include "Joystick.h" #include "JoystickManager.h" -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "MultiVehicleManager.h" #include "QGCLoggingCategory.h" #include "QGCVideoStreamInfo.h" @@ -231,10 +231,10 @@ void QGCCameraManager::_handleHeartbeat(const mavlink_message_t &message) pInfo->lastHeartbeat.start(); } -MavlinkCameraControl *QGCCameraManager::currentCameraInstance() +MavlinkCameraControlInterface *QGCCameraManager::currentCameraInstance() { if ((_currentCameraIndex < _cameras.count()) && !_cameras.isEmpty()) { - MavlinkCameraControl *pCamera = qobject_cast(_cameras[_currentCameraIndex]); + MavlinkCameraControlInterface *pCamera = qobject_cast(_cameras[_currentCameraIndex]); return pCamera; } return nullptr; @@ -242,7 +242,7 @@ MavlinkCameraControl *QGCCameraManager::currentCameraInstance() QGCVideoStreamInfo *QGCCameraManager::currentStreamInstance() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { QGCVideoStreamInfo *pInfo = pCamera->currentStreamInstance(); return pInfo; @@ -252,7 +252,7 @@ QGCVideoStreamInfo *QGCCameraManager::currentStreamInstance() QGCVideoStreamInfo *QGCCameraManager::thermalStreamInstance() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { QGCVideoStreamInfo *pInfo = pCamera->thermalStreamInstance(); return pInfo; @@ -260,15 +260,15 @@ QGCVideoStreamInfo *QGCCameraManager::thermalStreamInstance() return nullptr; } -MavlinkCameraControl *QGCCameraManager::_findCamera(int id) +MavlinkCameraControlInterface *QGCCameraManager::_findCamera(int id) { for (int i = 0; i < _cameras.count(); i++) { if (!_cameras[i]) { continue; } - MavlinkCameraControl *pCamera = qobject_cast(_cameras[i]); + MavlinkCameraControlInterface *pCamera = qobject_cast(_cameras[i]); if (!pCamera) { - qCCritical(CameraManagerLog) << "Invalid MavlinkCameraControl instance"; + qCCritical(CameraManagerLog) << "Invalid MavlinkCameraControlInterface instance"; continue; } if (pCamera->compID() == id) { @@ -280,7 +280,7 @@ MavlinkCameraControl *QGCCameraManager::_findCamera(int id) return nullptr; } -void QGCCameraManager::_addCameraControlToLists(MavlinkCameraControl *cameraControl) +void QGCCameraManager::_addCameraControlToLists(MavlinkCameraControlInterface *cameraControl) { if (qobject_cast(cameraControl)) { qCDebug(CameraManagerLog) << "Adding simulated camera to list"; @@ -319,9 +319,9 @@ void QGCCameraManager::_handleCameraInfo(const mavlink_message_t& message) mavlink_msg_camera_information_decode(&message, &info); qCDebug(CameraManagerLog) << "Camera information received from" << QGCMAVLink::compIdToString(message.compid) << "Model:" << reinterpret_cast(info.model_name); - qCDebug(CameraManagerLog) << "Creating MavlinkCameraControl for camera"; + qCDebug(CameraManagerLog) << "Creating MavlinkCameraControlInterface for camera"; - MavlinkCameraControl *pCamera = _vehicle->firmwarePlugin()->createCameraControl(&info, _vehicle, message.compid, this); + MavlinkCameraControlInterface *pCamera = _vehicle->firmwarePlugin()->createCameraControl(&info, _vehicle, message.compid, this); if (pCamera) { _addCameraControlToLists(pCamera); @@ -362,7 +362,7 @@ void QGCCameraManager::_checkForLostCameras() continue; } - MavlinkCameraControl* pCamera = _findCamera(pInfo->compID); + MavlinkCameraControlInterface* pCamera = _findCamera(pInfo->compID); if (pCamera) { const int idx = _cameras.indexOf(pCamera); if (idx >= 0) { @@ -397,7 +397,7 @@ void QGCCameraManager::_checkForLostCameras() void QGCCameraManager::_handleCameraCaptureStatus(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_camera_capture_status_t cap{}; mavlink_msg_camera_capture_status_decode(&message, &cap); @@ -407,7 +407,7 @@ void QGCCameraManager::_handleCameraCaptureStatus(const mavlink_message_t &messa void QGCCameraManager::_handleStorageInformation(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_storage_information_t st{}; mavlink_msg_storage_information_decode(&message, &st); @@ -441,7 +441,7 @@ void QGCCameraManager::_handleCameraSettings(const mavlink_message_t& message) void QGCCameraManager::_handleParamExtAck(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_param_ext_ack_t ack{}; mavlink_msg_param_ext_ack_decode(&message, &ack); @@ -451,7 +451,7 @@ void QGCCameraManager::_handleParamExtAck(const mavlink_message_t &message) void QGCCameraManager::_handleParamExtValue(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_param_ext_value_t value{}; mavlink_msg_param_ext_value_decode(&message, &value); @@ -461,7 +461,7 @@ void QGCCameraManager::_handleParamExtValue(const mavlink_message_t &message) void QGCCameraManager::_handleVideoStreamInformation(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_video_stream_information_t streamInfo{}; mavlink_msg_video_stream_information_decode(&message, &streamInfo); @@ -472,7 +472,7 @@ void QGCCameraManager::_handleVideoStreamInformation(const mavlink_message_t &me void QGCCameraManager::_handleVideoStreamStatus(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_video_stream_status_t streamStatus{}; mavlink_msg_video_stream_status_decode(&message, &streamStatus); @@ -482,7 +482,7 @@ void QGCCameraManager::_handleVideoStreamStatus(const mavlink_message_t &message void QGCCameraManager::_handleBatteryStatus(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_battery_status_t batteryStatus{}; mavlink_msg_battery_status_decode(&message, &batteryStatus); @@ -492,7 +492,7 @@ void QGCCameraManager::_handleBatteryStatus(const mavlink_message_t &message) void QGCCameraManager::_handleTrackingImageStatus(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_camera_tracking_image_status_t tis{}; mavlink_msg_camera_tracking_image_status_decode(&message, &tis); @@ -640,7 +640,7 @@ void QGCCameraManager::_activeJoystickChanged(Joystick *joystick) void QGCCameraManager::_triggerCamera() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->takePhoto(); } @@ -648,7 +648,7 @@ void QGCCameraManager::_triggerCamera() void QGCCameraManager::_startVideoRecording() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->startVideoRecording(); } @@ -656,7 +656,7 @@ void QGCCameraManager::_startVideoRecording() void QGCCameraManager::_stopVideoRecording() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->stopVideoRecording(); } @@ -664,7 +664,7 @@ void QGCCameraManager::_stopVideoRecording() void QGCCameraManager::_toggleVideoRecording() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->toggleVideoRecording(); } @@ -675,7 +675,7 @@ void QGCCameraManager::_stepZoom(int direction) if (_lastZoomChange.elapsed() > 40) { _lastZoomChange.start(); qCDebug(CameraManagerLog) << "Step Camera Zoom" << direction; - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->stepZoom(direction); } @@ -685,7 +685,7 @@ void QGCCameraManager::_stepZoom(int direction) void QGCCameraManager::_startZoom(int direction) { qCDebug(CameraManagerLog) << "Start Camera Zoom" << direction; - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->startZoom(direction); } @@ -694,7 +694,7 @@ void QGCCameraManager::_startZoom(int direction) void QGCCameraManager::_stopZoom() { qCDebug(CameraManagerLog) << "Stop Camera Zoom"; - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->stopZoom(); } @@ -719,7 +719,7 @@ void QGCCameraManager::_stepStream(int direction) { if (_lastCameraChange.elapsed() > 1000) { _lastCameraChange.start(); - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { qCDebug(CameraManagerLog) << "Step Camera Stream" << direction; int stream = pCamera->currentStream() + direction; diff --git a/src/Camera/QGCCameraManager.h b/src/Camera/QGCCameraManager.h index 6ce5a3a9bef1..5a7515d474e1 100644 --- a/src/Camera/QGCCameraManager.h +++ b/src/Camera/QGCCameraManager.h @@ -17,7 +17,7 @@ Q_DECLARE_LOGGING_CATEGORY(CameraManagerLog) class CameraMetaData; class Joystick; -class MavlinkCameraControl; +class MavlinkCameraControlInterface; class QGCCameraManagerTest; class QGCVideoStreamInfo; class SimulatedCameraControl; @@ -29,11 +29,11 @@ class QGCCameraManager : public QObject QML_ELEMENT QML_UNCREATABLE("") Q_MOC_INCLUDE("Joystick.h") - Q_MOC_INCLUDE("MavlinkCameraControl.h") + Q_MOC_INCLUDE("MavlinkCameraControlInterface.h") Q_PROPERTY(QmlObjectListModel* cameras READ cameras NOTIFY camerasChanged) Q_PROPERTY(QStringList cameraLabels READ cameraLabels NOTIFY cameraLabelsChanged) - Q_PROPERTY(MavlinkCameraControl* currentCameraInstance READ currentCameraInstance NOTIFY currentCameraChanged) + Q_PROPERTY(MavlinkCameraControlInterface* currentCameraInstance READ currentCameraInstance NOTIFY currentCameraChanged) Q_PROPERTY(int currentCamera READ currentCamera WRITE setCurrentCamera NOTIFY currentCameraChanged) Q_PROPERTY(int currentZoomLevel READ currentZoomLevel NOTIFY currentZoomLevelChanged) @@ -65,7 +65,7 @@ class QGCCameraManager : public QObject const QmlObjectListModel* cameras() const { return &_cameras; } QStringList cameraLabels() const { return _cameraLabels; } int currentCamera() const { return _currentCameraIndex; } - MavlinkCameraControl* currentCameraInstance(); + MavlinkCameraControlInterface* currentCameraInstance(); void setCurrentCamera(int sel); QGCVideoStreamInfo* currentStreamInstance(); QGCVideoStreamInfo* thermalStreamInstance(); @@ -112,7 +112,7 @@ private slots: void _setCurrentZoomLevel(int level); private: - MavlinkCameraControl* _findCamera(int id); + MavlinkCameraControlInterface* _findCamera(int id); void _requestCameraInfo(CameraStruct* cameraInfo); void _handleHeartbeat(const mavlink_message_t& message); void _handleCameraInfo(const mavlink_message_t& message); @@ -125,7 +125,7 @@ private slots: void _handleVideoStreamStatus(const mavlink_message_t& message); void _handleBatteryStatus(const mavlink_message_t& message); void _handleTrackingImageStatus(const mavlink_message_t& message); - void _addCameraControlToLists(MavlinkCameraControl* cameraControl); + void _addCameraControlToLists(MavlinkCameraControlInterface* cameraControl); void _handleCameraFovStatus(const mavlink_message_t& message); QPointer _vehicle; diff --git a/src/Camera/SimulatedCameraControl.cc b/src/Camera/SimulatedCameraControl.cc index e0ea5b6cf541..352891ccb154 100644 --- a/src/Camera/SimulatedCameraControl.cc +++ b/src/Camera/SimulatedCameraControl.cc @@ -9,7 +9,7 @@ QGC_LOGGING_CATEGORY(SimulatedCameraControlLog, "Camera.SimulatedCameraControl") SimulatedCameraControl::SimulatedCameraControl(Vehicle *vehicle, QObject *parent) - : MavlinkCameraControl(vehicle, parent) + : MavlinkCameraControlInterface(vehicle, parent) { qCDebug(SimulatedCameraControlLog) << this; @@ -216,7 +216,7 @@ bool SimulatedCameraControl::hasVideoStream() const return VideoManager::instance()->decoding(); } -MavlinkCameraControl::CaptureVideoState SimulatedCameraControl::captureVideoState() const +MavlinkCameraControlInterface::CaptureVideoState SimulatedCameraControl::captureVideoState() const { if (!capturesVideo()) { return CaptureVideoStateDisabled; @@ -230,7 +230,7 @@ MavlinkCameraControl::CaptureVideoState SimulatedCameraControl::captureVideoStat return CaptureVideoStateIdle; } -MavlinkCameraControl::CapturePhotosState SimulatedCameraControl::capturePhotosState() const +MavlinkCameraControlInterface::CapturePhotosState SimulatedCameraControl::capturePhotosState() const { if (_photoCaptureStatus() == PHOTO_CAPTURE_IN_PROGRESS) { return CapturePhotosStateCapturingSinglePhoto; @@ -241,7 +241,7 @@ MavlinkCameraControl::CapturePhotosState SimulatedCameraControl::capturePhotosSt return capturesPhotos() ? CapturePhotosStateIdle : CapturePhotosStateDisabled; } -void SimulatedCameraControl::setPhotoCaptureMode(MavlinkCameraControl::PhotoCaptureMode photoCaptureMode) +void SimulatedCameraControl::setPhotoCaptureMode(MavlinkCameraControlInterface::PhotoCaptureMode photoCaptureMode) { if (photoCaptureMode == PHOTO_CAPTURE_TIMELAPSE) { qCWarning(CameraControlLog) << "Time lapse capture not supported by simulated camera"; diff --git a/src/Camera/SimulatedCameraControl.h b/src/Camera/SimulatedCameraControl.h index bc31fa970550..f855af8547e4 100644 --- a/src/Camera/SimulatedCameraControl.h +++ b/src/Camera/SimulatedCameraControl.h @@ -3,7 +3,7 @@ #include #include -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" class QGCVideoStreamInfo; class Vehicle; @@ -12,7 +12,7 @@ class Vehicle; /// Video record if a manual stream is available /// Photo capture using DO_DIGICAM_CONTROL if the setting is enabled /// It does not support time lapse capture -class SimulatedCameraControl : public MavlinkCameraControl +class SimulatedCameraControl : public MavlinkCameraControlInterface { Q_OBJECT diff --git a/src/Camera/VehicleCameraControl.cc b/src/Camera/VehicleCameraControl.cc index 489010131c66..8160840e7295 100644 --- a/src/Camera/VehicleCameraControl.cc +++ b/src/Camera/VehicleCameraControl.cc @@ -97,7 +97,7 @@ static bool read_value(QDomNode& element, const char* tagName, QString& target) } VehicleCameraControl::VehicleCameraControl(const mavlink_camera_information_t *info, Vehicle* vehicle, int compID, QObject* parent) - : MavlinkCameraControl(vehicle, parent) + : MavlinkCameraControlInterface(vehicle, parent) , _compID(compID) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); @@ -230,7 +230,7 @@ bool VehicleCameraControl::capturesPhotos() const return _mavlinkCameraInfo.flags & (CAMERA_CAP_FLAGS_CAPTURE_IMAGE | CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM); } -MavlinkCameraControl::CaptureVideoState VehicleCameraControl::captureVideoState() const +MavlinkCameraControlInterface::CaptureVideoState VehicleCameraControl::captureVideoState() const { if (_videoCaptureStatus() == VIDEO_CAPTURE_STATUS_RUNNING || VideoManager::instance()->recording()) { return CaptureVideoStateCapturing; @@ -246,7 +246,7 @@ MavlinkCameraControl::CaptureVideoState VehicleCameraControl::captureVideoState( return CaptureVideoStateDisabled; } -MavlinkCameraControl::CapturePhotosState VehicleCameraControl::capturePhotosState() const +MavlinkCameraControlInterface::CapturePhotosState VehicleCameraControl::capturePhotosState() const { if (_photoCaptureStatus() == PHOTO_CAPTURE_IN_PROGRESS) { return CapturePhotosStateCapturingSinglePhoto; diff --git a/src/Camera/VehicleCameraControl.h b/src/Camera/VehicleCameraControl.h index a9cbe6a5a647..a7855d9175d1 100644 --- a/src/Camera/VehicleCameraControl.h +++ b/src/Camera/VehicleCameraControl.h @@ -1,6 +1,6 @@ #pragma once -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "QmlObjectListModel.h" class QGCVideoStreamInfo; @@ -35,7 +35,7 @@ class QGCCameraOptionRange : public QObject }; /// MAVLink Camera API controller - connected to a real mavlink v2 camera -class VehicleCameraControl : public MavlinkCameraControl +class VehicleCameraControl : public MavlinkCameraControlInterface { public: VehicleCameraControl(const mavlink_camera_information_t* info, Vehicle* vehicle, int compID, QObject* parent = nullptr); diff --git a/src/FirmwarePlugin/FirmwarePlugin.cc b/src/FirmwarePlugin/FirmwarePlugin.cc index b4dfe8a795f1..b71d271f2713 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.cc +++ b/src/FirmwarePlugin/FirmwarePlugin.cc @@ -291,7 +291,7 @@ QGCCameraManager *FirmwarePlugin::createCameraManager(Vehicle *vehicle) const return new QGCCameraManager(vehicle); } -MavlinkCameraControl *FirmwarePlugin::createCameraControl(const mavlink_camera_information_t *info, Vehicle *vehicle, int compID, QObject *parent) const +MavlinkCameraControlInterface *FirmwarePlugin::createCameraControl(const mavlink_camera_information_t *info, Vehicle *vehicle, int compID, QObject *parent) const { return new VehicleCameraControl(info, vehicle, compID, parent); } diff --git a/src/FirmwarePlugin/FirmwarePlugin.h b/src/FirmwarePlugin/FirmwarePlugin.h index 4e0dd535d030..fa3bd8a876b6 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.h +++ b/src/FirmwarePlugin/FirmwarePlugin.h @@ -12,7 +12,7 @@ class VehicleComponent; class AutoPilotPlugin; class Vehicle; -class MavlinkCameraControl; +class MavlinkCameraControlInterface; class QGCCameraManager; class Autotune; class LinkInterface; @@ -342,7 +342,7 @@ class FirmwarePlugin : public QObject virtual QGCCameraManager *createCameraManager(Vehicle *vehicle) const; /// Camera control. - virtual MavlinkCameraControl *createCameraControl(const mavlink_camera_information_t *info, Vehicle *vehicle, int compID, QObject *parent = nullptr) const; + virtual MavlinkCameraControlInterface *createCameraControl(const mavlink_camera_information_t *info, Vehicle *vehicle, int compID, QObject *parent = nullptr) const; /// Returns a pointer to a dictionary of firmware-specific FactGroups virtual QMap *factGroups() { return nullptr; } diff --git a/src/FlightMap/Widgets/PhotoVideoControl.qml b/src/FlightMap/Widgets/PhotoVideoControl.qml index bedac2ec0874..5e9f23339155 100644 --- a/src/FlightMap/Widgets/PhotoVideoControl.qml +++ b/src/FlightMap/Widgets/PhotoVideoControl.qml @@ -21,10 +21,10 @@ Rectangle { property var _activeVehicle: globals.activeVehicle property var _cameraManager: _activeVehicle.cameraManager property var _camera: _cameraManager.currentCameraInstance - property bool _cameraInPhotoMode: _camera.cameraMode === MavlinkCameraControl.CAM_MODE_PHOTO || _camera.cameraMode === MavlinkCameraControl.CAM_MODE_SURVEY + property bool _cameraInPhotoMode: _camera.cameraMode === MavlinkCameraControlInterface.CAM_MODE_PHOTO || _camera.cameraMode === MavlinkCameraControlInterface.CAM_MODE_SURVEY property bool _cameraInVideoMode: !_cameraInPhotoMode - property bool _videoCaptureIdle: _camera.captureVideoState === MavlinkCameraControl.CaptureVideoStateIdle - property bool _photoCaptureIdle: _camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateIdle + property bool _videoCaptureIdle: _camera.captureVideoState === MavlinkCameraControlInterface.CaptureVideoStateIdle + property bool _photoCaptureIdle: _camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateIdle QGCPalette { id: qgcPal; colorGroupEnabled: enabled } @@ -151,7 +151,7 @@ Rectangle { border.width: 1 border.color: videoCaptureButtonPalette.buttonBorder visible: (_camera.hasModes && _cameraInVideoMode) || (!_camera.hasModes && _camera.capturesVideo) - enabled: _camera.captureVideoState !== MavlinkCameraControl.CaptureVideoStateDisabled + enabled: _camera.captureVideoState !== MavlinkCameraControlInterface.CaptureVideoStateDisabled QGCPalette { id: videoCaptureButtonPalette; colorGroupEnabled: videoCaptureButton.enabled } @@ -174,7 +174,7 @@ Rectangle { border.width: 1 border.color: videoCaptureButtonPalette.buttonBorder - property bool _isCapturing: _camera.captureVideoState === MavlinkCameraControl.CaptureVideoStateCapturing + property bool _isCapturing: _camera.captureVideoState === MavlinkCameraControlInterface.CaptureVideoStateCapturing } MouseArea { @@ -227,7 +227,7 @@ Rectangle { border.width: 1 border.color: photoCaptureButtonPalette.buttonBorder visible: (_camera.hasModes && _cameraInPhotoMode) || (!_camera.hasModes && (_camera.hasVideoStream || _camera.capturesPhotos)) - enabled: _camera.capturePhotosState !== MavlinkCameraControl.CapturePhotosStateDisabled + enabled: _camera.capturePhotosState !== MavlinkCameraControlInterface.CapturePhotosStateDisabled QGCPalette { id: photoCaptureButtonPalette; colorGroupEnabled: photoCaptureButton.enabled } @@ -250,16 +250,16 @@ Rectangle { border.width: 1 border.color: photoCaptureButtonPalette.buttonBorder - property bool _isCapturing: _camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateCapturingSinglePhoto || - _camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateCapturingMultiplePhotos + property bool _isCapturing: _camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateCapturingSinglePhoto || + _camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateCapturingMultiplePhotos } MouseArea { anchors.fill: parent onClicked: { - if (_camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateCapturingMultiplePhotos) { + if (_camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateCapturingMultiplePhotos) { _camera.stopTakePhoto() - } else if (_camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateIdle) { + } else if (_camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateIdle) { _camera.takePhoto() } } @@ -304,7 +304,7 @@ Rectangle { Layout.alignment: Qt.AlignHCenter text: qsTr("Free: ") + _camera.storageFreeStr font.pointSize: ScreenTools.defaultFontPointSize - visible: _camera.storageStatus === MavlinkCameraControl.STORAGE_READY + visible: _camera.storageStatus === MavlinkCameraControlInterface.STORAGE_READY } QGCLabel { @@ -390,7 +390,7 @@ Rectangle { property bool _multipleMavlinkCameras: _cameraManager.cameras.count > 1 property bool _multipleMavlinkCameraStreams: _camera.streamLabels.length > 1 - property bool _cameraStorageSupported: _camera.storageStatus !== MavlinkCameraControl.STORAGE_NOT_SUPPORTED + property bool _cameraStorageSupported: _camera.storageStatus !== MavlinkCameraControlInterface.STORAGE_NOT_SUPPORTED property var _videoSettings: QGroundControl.settingsManager.videoSettings ColumnLayout { @@ -424,7 +424,7 @@ Rectangle { QGCLabel { text: qsTr("Blend Opacity") - visible: _camera.thermalStreamInstance && _camera.thermalMode === MavlinkCameraControl.THERMAL_BLEND + visible: _camera.thermalStreamInstance && _camera.thermalMode === MavlinkCameraControlInterface.THERMAL_BLEND onVisibleChanged: gridLayout.dynamicRows += visible ? 1 : -1 } @@ -445,7 +445,7 @@ Rectangle { QGCLabel { text: qsTr("Photo Interval (seconds)") - visible: _camera.capturesPhotos && _camera.photoCaptureMode === MavlinkCameraControl.PHOTO_CAPTURE_TIMELAPSE + visible: _camera.capturesPhotos && _camera.photoCaptureMode === MavlinkCameraControlInterface.PHOTO_CAPTURE_TIMELAPSE onVisibleChanged: gridLayout.dynamicRows += visible ? 1 : -1 } @@ -506,7 +506,7 @@ Rectangle { from: 0 value: _camera.thermalOpacity live: true - visible: _camera.thermalStreamInstance && _camera.thermalMode === MavlinkCameraControl.THERMAL_BLEND + visible: _camera.thermalStreamInstance && _camera.thermalMode === MavlinkCameraControlInterface.THERMAL_BLEND onValueChanged: _camera.thermalOpacity = value } @@ -582,7 +582,7 @@ Rectangle { value: _camera.photoLapse displayValue: true live: true - visible: _camera.capturesPhotos && _camera.photoCaptureMode === MavlinkCameraControl.PHOTO_CAPTURE_TIMELAPSE + visible: _camera.capturesPhotos && _camera.photoCaptureMode === MavlinkCameraControlInterface.PHOTO_CAPTURE_TIMELAPSE onValueChanged: _camera.photoLapse = value } diff --git a/src/FlyView/FlightDisplayViewVideo.qml b/src/FlyView/FlightDisplayViewVideo.qml index a517434d8ea8..5c3b5e8c935f 100644 --- a/src/FlyView/FlightDisplayViewVideo.qml +++ b/src/FlyView/FlightDisplayViewVideo.qml @@ -184,12 +184,12 @@ Item { Item { id: thermalItem width: height * QGroundControl.videoManager.thermalAspectRatio - height: _camera ? (_camera.thermalMode === MavlinkCameraControl.THERMAL_FULL ? parent.height : (_camera.thermalMode === MavlinkCameraControl.THERMAL_PIP ? ScreenTools.defaultFontPixelHeight * 12 : parent.height * _thermalHeightFactor)) : 0 + height: _camera ? (_camera.thermalMode === MavlinkCameraControlInterface.THERMAL_FULL ? parent.height : (_camera.thermalMode === MavlinkCameraControlInterface.THERMAL_PIP ? ScreenTools.defaultFontPixelHeight * 12 : parent.height * _thermalHeightFactor)) : 0 anchors.centerIn: parent - visible: QGroundControl.videoManager.hasThermal && _camera.thermalMode !== MavlinkCameraControl.THERMAL_OFF + visible: QGroundControl.videoManager.hasThermal && _camera.thermalMode !== MavlinkCameraControlInterface.THERMAL_OFF function pipOrNot() { if(_camera) { - if(_camera.thermalMode === MavlinkCameraControl.THERMAL_PIP) { + if(_camera.thermalMode === MavlinkCameraControlInterface.THERMAL_PIP) { anchors.centerIn = undefined anchors.top = parent.top anchors.topMargin = mainWindow.header.height + (ScreenTools.defaultFontPixelHeight * 0.5) @@ -215,7 +215,7 @@ Item { id: thermalVideo objectName: "thermalVideo" anchors.fill: parent - opacity: _camera ? (_camera.thermalMode === MavlinkCameraControl.THERMAL_BLEND ? _camera.thermalOpacity / 100 : 1.0) : 0 + opacity: _camera ? (_camera.thermalMode === MavlinkCameraControlInterface.THERMAL_BLEND ? _camera.thermalOpacity / 100 : 1.0) : 0 } } //-- Zoom diff --git a/src/VideoManager/VideoManager.cc b/src/VideoManager/VideoManager.cc index c96e4ff190a6..7f1b73a1d829 100644 --- a/src/VideoManager/VideoManager.cc +++ b/src/VideoManager/VideoManager.cc @@ -1,6 +1,6 @@ #include "VideoManager.h" #include "AppSettings.h" -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "MultiVehicleManager.h" #include "QGCApplication.h" #include "QGCCameraManager.h" @@ -703,7 +703,7 @@ void VideoManager::_setActiveVehicle(Vehicle *vehicle) (void) disconnect(_activeVehicle->vehicleLinkManager(), &VehicleLinkManager::communicationLostChanged, this, &VideoManager::_communicationLostChanged); auto cameraManager = _activeVehicle->cameraManager(); if (cameraManager) { - MavlinkCameraControl *pCamera = cameraManager->currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = cameraManager->currentCameraInstance(); if (pCamera) { pCamera->stopStream(); } @@ -721,7 +721,7 @@ void VideoManager::_setActiveVehicle(Vehicle *vehicle) (void) connect(_activeVehicle->vehicleLinkManager(), &VehicleLinkManager::communicationLostChanged, this, &VideoManager::_communicationLostChanged); if (_activeVehicle->cameraManager()) { (void) connect(_activeVehicle->cameraManager(), &QGCCameraManager::streamChanged, this, &VideoManager::_videoSourceChanged); - MavlinkCameraControl *pCamera = _activeVehicle->cameraManager()->currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = _activeVehicle->cameraManager()->currentCameraInstance(); if (pCamera) { pCamera->resumeStream(); } diff --git a/test/Camera/VehicleCameraControlTest.cc b/test/Camera/VehicleCameraControlTest.cc index a12f42aa8758..722a3bbba7d6 100644 --- a/test/Camera/VehicleCameraControlTest.cc +++ b/test/Camera/VehicleCameraControlTest.cc @@ -4,7 +4,7 @@ #include #include "LinkManager.h" -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "MockConfiguration.h" #include "MockLink.h" #include "MultiVehicleManager.h" @@ -158,9 +158,9 @@ void VehicleCameraControlTest::_testCameraCapFlags() // Find Camera 1 (MAV_COMP_ID_CAMERA) which has our configured flags. // Camera 2 (MAV_COMP_ID_CAMERA2) is always photo-only and not what we're testing. - MavlinkCameraControl* camera = nullptr; + MavlinkCameraControlInterface* camera = nullptr; for (int i = 0; i < cameraManager->cameras()->count(); i++) { - auto* cam = qobject_cast(cameraManager->cameras()->get(i)); + auto* cam = qobject_cast(cameraManager->cameras()->get(i)); if (cam && cam->compID() == MAV_COMP_ID_CAMERA) { camera = cam; break; From b47066a56ccdb9930f9e2decbe1996723145f82d Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 12 Mar 2026 14:46:10 -0700 Subject: [PATCH 083/119] Replace magic numbers with MAVLink enums in tracking handler Use CAMERA_TRACKING_STATUS_FLAGS_IDLE and CAMERA_TRACKING_MODE_RECTANGLE instead of raw numeric literals 0 and 2 in handleTrackingImageStatus(). --- src/Camera/VehicleCameraControl.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Camera/VehicleCameraControl.cc b/src/Camera/VehicleCameraControl.cc index 8160840e7295..e488c5958a95 100644 --- a/src/Camera/VehicleCameraControl.cc +++ b/src/Camera/VehicleCameraControl.cc @@ -1693,11 +1693,11 @@ void VehicleCameraControl::handleTrackingImageStatus(const mavlink_camera_tracki _trackingImageStatus = trackingImageStatus; - if (_trackingImageStatus.tracking_status == 0 || !trackingEnabled()) { + if (_trackingImageStatus.tracking_status == CAMERA_TRACKING_STATUS_FLAGS_IDLE || !trackingEnabled()) { _trackingImageRect = {}; qCDebug(CameraControlLog) << "Tracking off"; } else { - if (_trackingImageStatus.tracking_mode == 2) { + if (_trackingImageStatus.tracking_mode == CAMERA_TRACKING_MODE_RECTANGLE) { _trackingImageRect = QRectF(QPointF(_trackingImageStatus.rec_top_x, _trackingImageStatus.rec_top_y), QPointF(_trackingImageStatus.rec_bottom_x, _trackingImageStatus.rec_bottom_y)); } else { From 2f6b634b8d589ccd4753cb6b3d55865dfb860032 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 12 Mar 2026 14:47:02 -0700 Subject: [PATCH 084/119] Remove duplicate-suppression guards from startTracking The startTracking(QRectF) and startTracking(QPointF, double) overloads had guards that prevented re-sending the tracking command if called with the same coordinates. This caused clicks on the same spot to be silently dropped, making it impossible to re-track a target after tracking was lost or stopped. Remove the guards and the now-unused cached member variables (_trackingMarquee, _trackingPoint, _trackingRadius). Fixes #12193 --- src/Camera/VehicleCameraControl.cc | 59 ++++++++++++------------------ src/Camera/VehicleCameraControl.h | 3 -- 2 files changed, 24 insertions(+), 38 deletions(-) diff --git a/src/Camera/VehicleCameraControl.cc b/src/Camera/VehicleCameraControl.cc index e488c5958a95..0a62c0a3baed 100644 --- a/src/Camera/VehicleCameraControl.cc +++ b/src/Camera/VehicleCameraControl.cc @@ -2333,49 +2333,38 @@ void VehicleCameraControl::setTrackingEnabled(bool set) void VehicleCameraControl::startTracking(QRectF rec) { - if(_trackingMarquee != rec) { - _trackingMarquee = rec; + qCDebug(CameraControlLog) << "Start Tracking (Rectangle: [" + << static_cast(rec.x()) << ", " + << static_cast(rec.y()) << "] - [" + << static_cast(rec.x() + rec.width()) << ", " + << static_cast(rec.y() + rec.height()) << "]"; - qCDebug(CameraControlLog) << "Start Tracking (Rectangle: [" - << static_cast(rec.x()) << ", " - << static_cast(rec.y()) << "] - [" - << static_cast(rec.x() + rec.width()) << ", " - << static_cast(rec.y() + rec.height()) << "]"; - - _vehicle->sendMavCommand(_compID, - MAV_CMD_CAMERA_TRACK_RECTANGLE, - true, - static_cast(rec.x()), - static_cast(rec.y()), - static_cast(rec.x() + rec.width()), - static_cast(rec.y() + rec.height())); + _vehicle->sendMavCommand(_compID, + MAV_CMD_CAMERA_TRACK_RECTANGLE, + true, + static_cast(rec.x()), + static_cast(rec.y()), + static_cast(rec.x() + rec.width()), + static_cast(rec.y() + rec.height())); - // Request tracking status - _requestTrackingStatus(); - } + _requestTrackingStatus(); } void VehicleCameraControl::startTracking(QPointF point, double radius) { - if(_trackingPoint != point || _trackingRadius != radius) { - _trackingPoint = point; - _trackingRadius = radius; + qCDebug(CameraControlLog) << "Start Tracking (Point: [" + << static_cast(point.x()) << ", " + << static_cast(point.y()) << "], Radius: " + << static_cast(radius); - qCDebug(CameraControlLog) << "Start Tracking (Point: [" - << static_cast(point.x()) << ", " - << static_cast(point.y()) << "], Radius: " - << static_cast(radius); - - _vehicle->sendMavCommand(_compID, - MAV_CMD_CAMERA_TRACK_POINT, - true, - static_cast(point.x()), - static_cast(point.y()), - static_cast(radius)); + _vehicle->sendMavCommand(_compID, + MAV_CMD_CAMERA_TRACK_POINT, + true, + static_cast(point.x()), + static_cast(point.y()), + static_cast(radius)); - // Request tracking status - _requestTrackingStatus(); - } + _requestTrackingStatus(); } void VehicleCameraControl::stopTracking() diff --git a/src/Camera/VehicleCameraControl.h b/src/Camera/VehicleCameraControl.h index a7855d9175d1..041ed430d00a 100644 --- a/src/Camera/VehicleCameraControl.h +++ b/src/Camera/VehicleCameraControl.h @@ -294,9 +294,6 @@ protected slots: ThermalViewMode _thermalMode = THERMAL_BLEND; double _thermalOpacity = 85.0; TrackingStatus _trackingStatus = TRACKING_UNKNOWN; - QRectF _trackingMarquee; - QPointF _trackingPoint; - double _trackingRadius = 0.0; mavlink_camera_tracking_image_status_t _trackingImageStatus; QRectF _trackingImageRect; }; From 330125eaf9092b7d6c1cad61f8cf66c6f4c5dff8 Mon Sep 17 00:00:00 2001 From: Hamish Willee Date: Thu, 12 Mar 2026 11:34:54 +1100 Subject: [PATCH 085/119] docs(api_reference): Add Doxygen docs to sidebar and link in docs --- docs/en/SUMMARY.md | 1 + docs/en/qgc-dev-guide/classes/index.md | 4 ++++ docs/en/qgc-dev-guide/navigating_source.md | 9 ++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index 2bdb50744012..e5cc3d34955d 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -80,6 +80,7 @@ - [Getting Started with source & builds](qgc-dev-guide/getting_started/index.md) - [Build using Containers](qgc-dev-guide/getting_started/container.md) - [Navigating the Source Code](qgc-dev-guide/navigating_source.md) + - [API Reference (Doxygen)](https://api.qgroundcontrol.com/master/annotated.html) - [Class Hierarchy](qgc-dev-guide/classes/index.md) - [QGC Release/Branching Process](qgc-dev-guide/release_branching_process.md) - [Communication Flow](qgc-dev-guide/communication_flow.md) diff --git a/docs/en/qgc-dev-guide/classes/index.md b/docs/en/qgc-dev-guide/classes/index.md index 85f026674d99..9ce1d55bdfd6 100644 --- a/docs/en/qgc-dev-guide/classes/index.md +++ b/docs/en/qgc-dev-guide/classes/index.md @@ -1,5 +1,9 @@ # Class Hierarchy (high level) +::: tip +Doxygen documentation for classes can be found at . +::: + ## LinkManager, LinkInterface A "Link" in QGC is a specific type of communication pipe with the vehicle such as a serial port or UDP over WiFi. The base class for all links is LinkInterface. Each link runs on it's own thread and sends bytes to MAVLinkProtocol. diff --git a/docs/en/qgc-dev-guide/navigating_source.md b/docs/en/qgc-dev-guide/navigating_source.md index b2dec3c69e18..3cf93a536c61 100644 --- a/docs/en/qgc-dev-guide/navigating_source.md +++ b/docs/en/qgc-dev-guide/navigating_source.md @@ -1,6 +1,13 @@ # Navigating QGC Source Code -QGC is a large code base. With that it can be daunting to find what your are looking for in the source. Below are listed some tips to help you find what you are looking for. +QGC is a large code base and it can be daunting to find what your are looking for in the source. + +Below are listed some tips to help you find what you are looking for. + +## Use the API Reference + +Doxygen documentation can be found at . +This is can be easier to navigate and search than the source itself. ## Start from the top of the UI From d75f0a7d5f841a92a31e27252384fb2732bc8087 Mon Sep 17 00:00:00 2001 From: Hamish Willee Date: Fri, 13 Mar 2026 07:50:23 +1100 Subject: [PATCH 086/119] Update docs/en/qgc-dev-guide/navigating_source.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/en/qgc-dev-guide/navigating_source.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/qgc-dev-guide/navigating_source.md b/docs/en/qgc-dev-guide/navigating_source.md index 3cf93a536c61..118eb71a049c 100644 --- a/docs/en/qgc-dev-guide/navigating_source.md +++ b/docs/en/qgc-dev-guide/navigating_source.md @@ -1,6 +1,6 @@ # Navigating QGC Source Code -QGC is a large code base and it can be daunting to find what your are looking for in the source. +QGC is a large code base and it can be daunting to find what you are looking for in the source. Below are listed some tips to help you find what you are looking for. From e0363abbeeb1b43e620afcbeecca5dd9ad8e8566 Mon Sep 17 00:00:00 2001 From: Hamish Willee Date: Fri, 13 Mar 2026 07:50:30 +1100 Subject: [PATCH 087/119] Update docs/en/qgc-dev-guide/navigating_source.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/en/qgc-dev-guide/navigating_source.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/qgc-dev-guide/navigating_source.md b/docs/en/qgc-dev-guide/navigating_source.md index 118eb71a049c..c04ffab06950 100644 --- a/docs/en/qgc-dev-guide/navigating_source.md +++ b/docs/en/qgc-dev-guide/navigating_source.md @@ -7,7 +7,7 @@ Below are listed some tips to help you find what you are looking for. ## Use the API Reference Doxygen documentation can be found at . -This is can be easier to navigate and search than the source itself. +This can be easier to navigate and search than the source itself. ## Start from the top of the UI From 0a460b9b2d384c2f36aec53bfd83c323d6012672 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 12 Mar 2026 18:31:36 -0700 Subject: [PATCH 088/119] Standardize ComponentSummary QML layout pattern --- .../APM/APMAirframeComponentSummary.qml | 10 ++- .../APM/APMFlightModesComponentSummary.qml | 10 ++- .../APM/APMFollowComponentSummary.qml | 10 ++- .../APM/APMLightsComponentSummary.qml | 10 ++- .../APM/APMPowerComponentSummary.qml | 10 ++- .../APM/APMRadioComponentSummary.qml | 10 ++- .../APM/APMSafetyComponentSummary.qml | 10 ++- .../APM/APMSensorsComponentSummary.qml | 13 ++-- .../APM/APMSubFrameComponentSummary.qml | 11 ++- .../Common/ESP8266ComponentSummary.qml | 11 ++- .../Common/JoystickComponentSummary.qml | 10 ++- .../PX4/AirframeComponentSummary.qml | 11 ++- .../PX4/FlightModesComponentSummary.qml | 11 ++- .../PX4/PX4RadioComponentSummary.qml | 10 ++- .../PX4/PowerComponentSummary.qml | 10 ++- .../PX4/SafetyComponentSummary.qml | 10 ++- .../PX4/SensorsComponentSummary.qml | 10 ++- src/QmlControls/VehicleSummaryRow.qml | 19 ++--- src/Vehicle/VehicleSetup/VehicleSummary.qml | 76 ++++++++++--------- 19 files changed, 176 insertions(+), 96 deletions(-) diff --git a/src/AutoPilotPlugins/APM/APMAirframeComponentSummary.qml b/src/AutoPilotPlugins/APM/APMAirframeComponentSummary.qml index 790dabe2d7fe..48495cf8216b 100644 --- a/src/AutoPilotPlugins/APM/APMAirframeComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMAirframeComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth APMAirframeComponentController {id: controller; } @@ -14,8 +17,9 @@ Item { property Fact _frameType: controller.getParameterFact(-1, "FRAME_TYPE", false) property bool _frameTypeAvailable: controller.parameterExists(-1, "FRAME_TYPE") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Frame Class") diff --git a/src/AutoPilotPlugins/APM/APMFlightModesComponentSummary.qml b/src/AutoPilotPlugins/APM/APMFlightModesComponentSummary.qml index edfbf81327ac..109b1d930254 100644 --- a/src/AutoPilotPlugins/APM/APMFlightModesComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMFlightModesComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -20,8 +23,9 @@ Item { property Fact flightMode5: controller.getParameterFact(-1, _roverFirmware ? "MODE5" : "FLTMODE5") property Fact flightMode6: controller.getParameterFact(-1, _roverFirmware ? "MODE6" : "FLTMODE6") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Flight Mode 1") diff --git a/src/AutoPilotPlugins/APM/APMFollowComponentSummary.qml b/src/AutoPilotPlugins/APM/APMFollowComponentSummary.qml index 22f0d36ea80b..9b253fe49b7f 100644 --- a/src/AutoPilotPlugins/APM/APMFollowComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMFollowComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller } @@ -28,8 +31,9 @@ Item { { label: qsTr("Yaw Behavior"), fact: getFact("FOLL_YAW_BEHAVE"), visible: followParamsAvailable } ] - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 Repeater { model: followItems diff --git a/src/AutoPilotPlugins/APM/APMLightsComponentSummary.qml b/src/AutoPilotPlugins/APM/APMLightsComponentSummary.qml index 3dd830eb7e1a..aecdf55cc397 100644 --- a/src/AutoPilotPlugins/APM/APMLightsComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMLightsComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -81,8 +84,9 @@ Item { property int lights2Function: _rcFunctionRCIN10 } - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Lights Output 1") diff --git a/src/AutoPilotPlugins/APM/APMPowerComponentSummary.qml b/src/AutoPilotPlugins/APM/APMPowerComponentSummary.qml index 6ad00799681f..dc6e21bb1d35 100644 --- a/src/AutoPilotPlugins/APM/APMPowerComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMPowerComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -19,8 +22,9 @@ Item { property Fact _batt2Capacity: controller.getParameterFact(-1, "BATT2_CAPACITY", false /* reportMissing */) property bool _battCapacityAvailable: controller.parameterExists(-1, "BATT_CAPACITY") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Batt1 monitor") diff --git a/src/AutoPilotPlugins/APM/APMRadioComponentSummary.qml b/src/AutoPilotPlugins/APM/APMRadioComponentSummary.qml index 4df3795f4fcb..6618b48ed8f6 100644 --- a/src/AutoPilotPlugins/APM/APMRadioComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMRadioComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -15,8 +18,9 @@ Item { property Fact mapYawFact: controller.getParameterFact(-1, "RCMAP_YAW") property Fact mapThrottleFact: controller.getParameterFact(-1, "RCMAP_THROTTLE") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Roll") diff --git a/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml b/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml index bba18a214385..4d6ba962227d 100644 --- a/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -29,8 +32,9 @@ Item { property bool _roverFirmware: controller.parameterExists(-1, "MODE1") // This catches all usage of ArduRover firmware vehicle types: Rover, Boat... - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Arming Checks:") diff --git a/src/AutoPilotPlugins/APM/APMSensorsComponentSummary.qml b/src/AutoPilotPlugins/APM/APMSensorsComponentSummary.qml index ed470b85f4a0..c4f07c75dc94 100644 --- a/src/AutoPilotPlugins/APM/APMSensorsComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMSensorsComponentSummary.qml @@ -11,7 +11,9 @@ import QGroundControl.Controls */ Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth APMSensorsComponentController { id: controller; } @@ -20,8 +22,9 @@ Item { factPanelController: controller } - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Compasses:") @@ -77,7 +80,7 @@ Item { model: sensorParams.rgInsId.length APMSensorIdDecoder { fact: sensorParams.rgInsId[index] - anchors.right: parent.right + Layout.alignment: Qt.AlignRight } } @@ -90,7 +93,7 @@ Item { model: sensorParams.rgBaroId.length APMSensorIdDecoder { fact: sensorParams.rgBaroId[index] - anchors.right: parent.right + Layout.alignment: Qt.AlignRight } } } diff --git a/src/AutoPilotPlugins/APM/APMSubFrameComponentSummary.qml b/src/AutoPilotPlugins/APM/APMSubFrameComponentSummary.qml index c0261892f00f..8c871aec0eaa 100644 --- a/src/AutoPilotPlugins/APM/APMSubFrameComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMSubFrameComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -35,8 +38,10 @@ Item { } } - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 + VehicleSummaryRow { id: nameRow; labelText: qsTr("Frame Type") diff --git a/src/AutoPilotPlugins/Common/ESP8266ComponentSummary.qml b/src/AutoPilotPlugins/Common/ESP8266ComponentSummary.qml index d49c407b7359..23485c204cf0 100644 --- a/src/AutoPilotPlugins/Common/ESP8266ComponentSummary.qml +++ b/src/AutoPilotPlugins/Common/ESP8266ComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -21,8 +24,10 @@ Item { property Fact uartBaud: controller.getParameterFact(esp8266.componentID, "UART_BAUDRATE") property Fact wifiMode: controller.getParameterFact(esp8266.componentID, "WIFI_MODE", false) //-- Don't bitch if missing - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 + VehicleSummaryRow { labelText: qsTr("Firmware Version") valueText: esp8266.version diff --git a/src/AutoPilotPlugins/Common/JoystickComponentSummary.qml b/src/AutoPilotPlugins/Common/JoystickComponentSummary.qml index b314c4471fd8..878a254e5c26 100644 --- a/src/AutoPilotPlugins/Common/JoystickComponentSummary.qml +++ b/src/AutoPilotPlugins/Common/JoystickComponentSummary.qml @@ -1,17 +1,21 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth readonly property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle readonly property var _activeJoystick: joystickManager.activeJoystick - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Status") diff --git a/src/AutoPilotPlugins/PX4/AirframeComponentSummary.qml b/src/AutoPilotPlugins/PX4/AirframeComponentSummary.qml index 1df984dfab6d..c8e587012ee1 100644 --- a/src/AutoPilotPlugins/PX4/AirframeComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/AirframeComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth AirframeComponentController { id: controller; } @@ -15,8 +18,10 @@ Item { property bool autoStartSet: sysAutoStartFact ? (sysAutoStartFact.value !== 0) : false - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 + VehicleSummaryRow { labelText: qsTr("System ID") valueText: sysIdFact ? sysIdFact.valueString : "" diff --git a/src/AutoPilotPlugins/PX4/FlightModesComponentSummary.qml b/src/AutoPilotPlugins/PX4/FlightModesComponentSummary.qml index 815b5f1fe4bc..5fdd09f06bf3 100644 --- a/src/AutoPilotPlugins/PX4/FlightModesComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/FlightModesComponentSummary.qml @@ -1,20 +1,25 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } property Fact _nullFact property Fact _rcMapFltmode: controller.parameterExists(-1, "RC_MAP_FLTMODE") ? controller.getParameterFact(-1, "RC_MAP_FLTMODE") : _nullFact - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 + VehicleSummaryRow { labelText: qsTr("Mode switch") valueText: _rcMapFltmode.value === 0 ? qsTr("Setup required") : _rcMapFltmode.enumStringValue diff --git a/src/AutoPilotPlugins/PX4/PX4RadioComponentSummary.qml b/src/AutoPilotPlugins/PX4/PX4RadioComponentSummary.qml index 73f62e09095a..3354dfc786a3 100644 --- a/src/AutoPilotPlugins/PX4/PX4RadioComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/PX4RadioComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -18,8 +21,9 @@ Item { property Fact mapAux1Fact: controller.getParameterFact(-1, "RC_MAP_AUX1") property Fact mapAux2Fact: controller.getParameterFact(-1, "RC_MAP_AUX2") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Roll") diff --git a/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml b/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml index e02375611953..c5ace0108e26 100644 --- a/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth property string _naString: qsTr("N/A") @@ -18,8 +21,9 @@ Item { batteryIndex: 1 } - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Battery Source") diff --git a/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml b/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml index 187726b64237..5a94367ff3ca 100644 --- a/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -20,8 +23,9 @@ Item { property Fact _rtlLandDelayFact: controller.getParameterFact(-1, "RTL_LAND_DELAY") property int _rtlLandDelayValue: _rtlLandDelayFact.value - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Low Battery Failsafe") diff --git a/src/AutoPilotPlugins/PX4/SensorsComponentSummary.qml b/src/AutoPilotPlugins/PX4/SensorsComponentSummary.qml index e3b5a542c8db..3382b902dd1e 100644 --- a/src/AutoPilotPlugins/PX4/SensorsComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/SensorsComponentSummary.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls @@ -10,7 +11,9 @@ import QGroundControl.Controls */ Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -20,8 +23,9 @@ Item { property Fact gyro0IdFact: controller.getParameterFact(-1, "CAL_GYRO0_ID") property Fact accel0IdFact: controller.getParameterFact(-1, "CAL_ACC0_ID") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Compass 0") diff --git a/src/QmlControls/VehicleSummaryRow.qml b/src/QmlControls/VehicleSummaryRow.qml index 533d00549cb3..491b9e2c512d 100644 --- a/src/QmlControls/VehicleSummaryRow.qml +++ b/src/QmlControls/VehicleSummaryRow.qml @@ -7,22 +7,23 @@ import QGroundControl.Controls RowLayout { id: root + Layout.fillWidth: true + spacing: ScreenTools.defaultFontPixelHeight property string labelText: "Label" property string valueText: "value" property string valueColor: "" - width: parent.width - QGCLabel { - id: label - text: root.labelText + id: label + Layout.fillWidth: true + text: root.labelText } + QGCLabel { - text: root.valueText - color: root.valueColor !== "" ? root.valueColor : QGroundControl.globalPalette.text - elide: Text.ElideRight - horizontalAlignment: Text.AlignRight - Layout.fillWidth: true + Layout.maximumWidth: ScreenTools.defaultFontPixelWidth * 20 + text: root.valueText + color: root.valueColor !== "" ? root.valueColor : QGroundControl.globalPalette.text + elide: Text.ElideRight } } diff --git a/src/Vehicle/VehicleSetup/VehicleSummary.qml b/src/Vehicle/VehicleSetup/VehicleSummary.qml index 305ec83a2f40..5b93bb705891 100644 --- a/src/Vehicle/VehicleSetup/VehicleSummary.qml +++ b/src/Vehicle/VehicleSetup/VehicleSummary.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.Controls @@ -14,6 +15,7 @@ Rectangle { property real _minSummaryW: ScreenTools.isTinyScreen ? ScreenTools.defaultFontPixelWidth * 28 : ScreenTools.defaultFontPixelWidth * 36 property real _summaryBoxWidth: _minSummaryW property real _summaryBoxSpace: ScreenTools.defaultFontPixelWidth * 2 + property real _margins: ScreenTools.defaultFontPixelHeight / 2 function computeSummaryBoxSize() { var sw = 0 @@ -85,52 +87,58 @@ Rectangle { // Outer summary item rectangle Rectangle { - width: _summaryBoxWidth - height: ScreenTools.defaultFontPixelHeight * 13 - color: qgcPal.windowShade - visible: modelData.summaryQmlSource.toString() !== "" + width: mainLayout.width + (_margins * 2) + height: mainLayout.height + (_margins * 2) + color: qgcPal.windowShade + visible: modelData.summaryQmlSource.toString() !== "" border.width: 1 border.color: qgcPal.text + Component.onCompleted: { border.color = Qt.rgba(border.color.r, border.color.g, border.color.b, 0.1) } readonly property real titleHeight: ScreenTools.defaultFontPixelHeight * 2 - // Title bar - QGCButton { - id: titleBar - width: parent.width - height: titleHeight - text: capitalizeWords(modelData.name) - - // Setup indicator - Rectangle { - anchors.rightMargin: ScreenTools.defaultFontPixelWidth - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - width: ScreenTools.defaultFontPixelWidth * 1.75 - height: width - radius: width / 2 - color: modelData.setupComplete ? "#00d932" : "red" - visible: modelData.requiresSetup && modelData.setupSource !== "" - } + ColumnLayout { + id: mainLayout + anchors.margins: _margins + anchors.left: parent.left + anchors.top: parent.top + spacing: ScreenTools.defaultFontPixelHeight / 2 + + // Title bar + QGCButton { + Layout.fillWidth: true + Layout.preferredHeight: titleHeight + text: capitalizeWords(modelData.name) + + // Setup indicator + Rectangle { + anchors.rightMargin: ScreenTools.defaultFontPixelWidth + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: ScreenTools.defaultFontPixelWidth * 1.75 + height: width + radius: width / 2 + color: modelData.setupComplete ? "#00d932" : "red" + visible: modelData.requiresSetup && modelData.setupSource !== "" + } - onClicked : { - //console.log(modelData.setupSource) - if (modelData.setupSource !== "") { - setupView.showVehicleComponentPanel(modelData) + onClicked : { + if (modelData.setupSource !== "") { + setupView.showVehicleComponentPanel(modelData) + } } } - } - // Summary Qml - Rectangle { - anchors.top: titleBar.bottom - width: parent.width + + // Summary Qml Loader { - anchors.fill: parent - anchors.margins: ScreenTools.defaultFontPixelWidth - source: modelData.summaryQmlSource + id: summaryLoader + Layout.fillWidth: true + Layout.preferredWidth: item ? item.implicitWidth : 0 + Layout.preferredHeight: item ? item.implicitHeight : 0 + source: modelData.summaryQmlSource property var vehicleComponent: modelData } From 03b10b674772c7dba074f75a8b1c28b80ec69bca Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 13 Mar 2026 10:57:56 -0700 Subject: [PATCH 089/119] guidedModeGotoLocation: return bool to indicate failure Change guidedModeGotoLocation from void to bool across FirmwarePlugin base class, PX4FirmwarePlugin, APMFirmwarePlugin, and Vehicle. Returns false when the goto command cannot be sent (unsupported guided mode, invalid vehicle position, target too far). The QML layer uses the return value to restore the goto map indicator to its previous position when the command fails, instead of leaving it at the rejected location. Closes #12834 --- src/FirmwarePlugin/APM/APMFirmwarePlugin.cc | 8 +++++--- src/FirmwarePlugin/APM/APMFirmwarePlugin.h | 2 +- src/FirmwarePlugin/FirmwarePlugin.cc | 3 ++- src/FirmwarePlugin/FirmwarePlugin.h | 3 ++- src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc | 6 ++++-- src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h | 2 +- src/FlyView/FlyViewMap.qml | 7 +++++-- src/FlyView/GuidedActionConfirm.qml | 8 ++++++-- src/FlyView/GuidedActionsController.qml | 16 +++++++++++----- src/Vehicle/Vehicle.cc | 11 ++++++----- src/Vehicle/Vehicle.h | 3 ++- 11 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc b/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc index fccea19e79a2..84e6ed1696c3 100644 --- a/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc @@ -793,11 +793,11 @@ static void _MAV_CMD_DO_REPOSITION_ResultHandler(void *resultHandlerData, int /* delete data; } -void APMFirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius) const +bool APMFirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius) const { if (qIsNaN(vehicle->altitudeRelative()->rawValue().toDouble())) { qgcApp()->showAppMessage(QStringLiteral("Unable to go to location, vehicle position not known.")); - return; + return false; } // attempt to use MAV_CMD_DO_REPOSITION to move vehicle. If that @@ -846,7 +846,7 @@ void APMFirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoord } if (instanceData->MAV_CMD_DO_REPOSITION_supported) { // no need to fall back - return; + return true; } } @@ -855,6 +855,8 @@ void APMFirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoord QGeoCoordinate coordWithAltitude = gotoCoord; coordWithAltitude.setAltitude(vehicle->altitudeRelative()->rawValue().toDouble()); vehicle->missionManager()->writeArduPilotGuidedMissionItem(coordWithAltitude, false /* altChangeOnly */); + + return true; } void APMFirmwarePlugin::guidedModeRTL(Vehicle *vehicle, bool smartRTL) const diff --git a/src/FirmwarePlugin/APM/APMFirmwarePlugin.h b/src/FirmwarePlugin/APM/APMFirmwarePlugin.h index 196236ee3f60..7c786f303080 100644 --- a/src/FirmwarePlugin/APM/APMFirmwarePlugin.h +++ b/src/FirmwarePlugin/APM/APMFirmwarePlugin.h @@ -31,7 +31,7 @@ class APMFirmwarePlugin : public FirmwarePlugin bool isCapable(const Vehicle *vehicle, FirmwareCapabilities capabilities) const override; void setGuidedMode(Vehicle *vehicle, bool guidedMode) const override; void guidedModeTakeoff(Vehicle *vehicle, double altitudeRel) const override; - void guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const override; + bool guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const override; double minimumTakeoffAltitudeMeters(Vehicle *vehicle) const override; void startTakeoff(Vehicle *vehicle) const override; void startMission(Vehicle *vehicle) const override; diff --git a/src/FirmwarePlugin/FirmwarePlugin.cc b/src/FirmwarePlugin/FirmwarePlugin.cc index b71d271f2713..c081abc57375 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.cc +++ b/src/FirmwarePlugin/FirmwarePlugin.cc @@ -141,12 +141,13 @@ void FirmwarePlugin::guidedModeTakeoff(Vehicle *vehicle, double takeoffAltRel) c qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); } -void FirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius) const +bool FirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius) const { Q_UNUSED(vehicle); Q_UNUSED(gotoCoord); Q_UNUSED(forwardFlightLoiterRadius); qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); + return false; } void FirmwarePlugin::guidedModeChangeAltitude(Vehicle*, double, bool pauseVehicle) diff --git a/src/FirmwarePlugin/FirmwarePlugin.h b/src/FirmwarePlugin/FirmwarePlugin.h index fa3bd8a876b6..96997e4f3ae4 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.h +++ b/src/FirmwarePlugin/FirmwarePlugin.h @@ -208,7 +208,8 @@ class FirmwarePlugin : public QObject virtual void startMission(Vehicle *vehicle) const; /// Command vehicle to move to specified location (altitude is included and relative) - virtual void guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius = 0.0) const; + /// @return true: goto command accepted, false: goto failed (vehicle not moved) + virtual bool guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius = 0.0) const; /// Command vehicle to change altitude /// @param altitudeChange If > 0, go up by amount specified, if < 0, go down by amount specified diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc index c7320834d6ca..644630d1ccca 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc @@ -387,7 +387,7 @@ bool PX4FirmwarePlugin::fixedWingAirSpeedLimitsAvailable(Vehicle* vehicle) const vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "FW_AIRSPD_MAX"); } -void PX4FirmwarePlugin::guidedModeGotoLocation(Vehicle* vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const +bool PX4FirmwarePlugin::guidedModeGotoLocation(Vehicle* vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const { // PX4 doesn't support setting the forward flight loiter radius of // MAV_CMD_DO_REPOSITION @@ -395,7 +395,7 @@ void PX4FirmwarePlugin::guidedModeGotoLocation(Vehicle* vehicle, const QGeoCoord if (qIsNaN(vehicle->altitudeAMSL()->rawValue().toDouble())) { qgcApp()->showAppMessage(tr("Unable to go to location, vehicle position not known.")); - return; + return false; } if (vehicle->capabilityBits() & MAV_PROTOCOL_CAPABILITY_COMMAND_INT) { @@ -422,6 +422,8 @@ void PX4FirmwarePlugin::guidedModeGotoLocation(Vehicle* vehicle, const QGeoCoord static_cast(gotoCoord.longitude()), vehicle->altitudeAMSL()->rawValue().toFloat()); } + + return true; } typedef struct { diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h index 2713af1fb481..760c78efec8b 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h @@ -42,7 +42,7 @@ class PX4FirmwarePlugin : public FirmwarePlugin double minimumEquivalentAirspeed(Vehicle* vehicle) const override; bool mulirotorSpeedLimitsAvailable(Vehicle* vehicle) const override; bool fixedWingAirSpeedLimitsAvailable(Vehicle* vehicle) const override; - void guidedModeGotoLocation (Vehicle* vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const override; + bool guidedModeGotoLocation (Vehicle* vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const override; void guidedModeChangeAltitude (Vehicle* vehicle, double altitudeRel, bool pauseVehicle) override; void guidedModeChangeGroundSpeedMetersSecond(Vehicle* vehicle, double groundspeed) const override; void guidedModeChangeEquivalentAirspeedMetersSecond(Vehicle* vehicle, double airspeed_equiv) const override; diff --git a/src/FlyView/FlyViewMap.qml b/src/FlyView/FlyViewMap.qml index 765dfb746035..dfd7753bf129 100644 --- a/src/FlyView/FlyViewMap.qml +++ b/src/FlyView/FlyViewMap.qml @@ -681,8 +681,11 @@ FlightMap { gotoLocationItem.show(mapClickCoord) if ((_activeVehicle.flightMode == _activeVehicle.gotoFlightMode) && !_flyViewSettings.goToLocationRequiresConfirmInGuided.value) { - globals.guidedControllerFlyView.executeAction(globals.guidedControllerFlyView.actionGoto, mapClickCoord, gotoLocationItem) - gotoLocationItem.actionConfirmed() // Still need to call this to commit the new coordinate and radius + if (globals.guidedControllerFlyView.executeAction(globals.guidedControllerFlyView.actionGoto, mapClickCoord)) { + gotoLocationItem.actionConfirmed() // Still need to call this to commit the new coordinate and radius + } else { + gotoLocationItem.actionCancelled() + } } else { globals.guidedControllerFlyView.confirmAction(globals.guidedControllerFlyView.actionGoto, mapClickCoord, gotoLocationItem) } diff --git a/src/FlyView/GuidedActionConfirm.qml b/src/FlyView/GuidedActionConfirm.qml index b08ceb43af42..0f58cb85249e 100644 --- a/src/FlyView/GuidedActionConfirm.qml +++ b/src/FlyView/GuidedActionConfirm.qml @@ -90,9 +90,13 @@ Item { guidedValueSlider.visible = false } hideTrigger = false - guidedController.executeAction(control.action, control.actionData, sliderOutputValue, control.optionChecked) + let success = guidedController.executeAction(control.action, control.actionData, sliderOutputValue, control.optionChecked) if (mapIndicator) { - mapIndicator.actionConfirmed() + if (success) { + mapIndicator.actionConfirmed() + } else { + mapIndicator.actionCancelled() + } mapIndicator = undefined } } diff --git a/src/FlyView/GuidedActionsController.qml b/src/FlyView/GuidedActionsController.qml index 87394adb9b84..b4679f5e878f 100644 --- a/src/FlyView/GuidedActionsController.qml +++ b/src/FlyView/GuidedActionsController.qml @@ -549,6 +549,7 @@ Item { } // Executes the specified action + // Returns false if the action failed and any associated map indicator should be restored function executeAction(actionCode, actionData, sliderOutputValue, optionChecked) { var i; var selectedVehicles; @@ -614,19 +615,23 @@ Item { _activeVehicle.guidedModeChangeAltitude(altitudeChangeInMeters, false /* pauseVehicle */) break case actionChangeLoiterRadius: - _activeVehicle.guidedModeGotoLocation( + if (!_activeVehicle.guidedModeGotoLocation( fwdFlightGotoMapCircle.coordinate, (fwdFlightGotoMapCircle.clockwiseRotation ? 1 : -1) * Math.abs(fwdFlightGotoMapCircle.radius.rawValue) - ) + )) { + return false + } break case actionGoto: - _activeVehicle.guidedModeGotoLocation( + if (!_activeVehicle.guidedModeGotoLocation( actionData, _vehicleInFwdFlight /* forwardFlightLoiterRadius */ ? _flyViewSettings.forwardFlightGoToLocationLoiterRad.value : 0 - ) + )) { + return false + } break case actionSetWaypoint: _activeVehicle.setCurrentMissionSequence(actionData) @@ -678,9 +683,10 @@ Item { default: if (!customController.customExecuteAction(actionCode, actionData, sliderOutputValue, optionChecked)) { console.warn(qsTr("Internal error: unknown actionCode"), actionCode) - return + return false } break } + return true } } diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index 6038559b0919..beeee047aacf 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -2030,21 +2030,22 @@ void Vehicle::startMission() _firmwarePlugin->startMission(this); } -void Vehicle::guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) +bool Vehicle::guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) { if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); - return; + return false; } if (!coordinate().isValid()) { - return; + return false; } double maxDistance = SettingsManager::instance()->flyViewSettings()->maxGoToLocationDistance()->rawValue().toDouble(); if (coordinate().distanceTo(gotoCoord) > maxDistance) { qgcApp()->showAppMessage(QString("New location is too far. Must be less than %1 %2.").arg(qRound(FactMetaData::metersToAppSettingsHorizontalDistanceUnits(maxDistance).toDouble())).arg(FactMetaData::appSettingsHorizontalDistanceUnitsString())); - return; + return false; } - _firmwarePlugin->guidedModeGotoLocation(this, gotoCoord, forwardFlightLoiterRadius); + + return _firmwarePlugin->guidedModeGotoLocation(this, gotoCoord, forwardFlightLoiterRadius); } void Vehicle::guidedModeChangeAltitude(double altitudeChange, bool pauseVehicle) diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index 53641d87f970..30287017594f 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -299,7 +299,8 @@ class Vehicle : public VehicleFactGroup Q_INVOKABLE double minimumEquivalentAirspeed(); /// Command vehicle to move to specified location (altitude is ignored) - Q_INVOKABLE void guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius = 0.0f); + /// @return true: goto command accepted, false: goto failed + Q_INVOKABLE bool guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius = 0.0f); /// Command vehicle to change altitude /// @param altitudeChange If > 0, go up by amount specified, if < 0, go down by amount specified From 8e12b37c9dcdca71572b72574558a0e2c4b846f7 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 13 Mar 2026 10:58:17 -0700 Subject: [PATCH 090/119] PlanMasterController: fix false dirty state on empty vehicle connect _loadRallyPointsComplete unconditionally set dirtyForSave=true whenever rally point loading completed, including during automatic initial plan load from a newly connected vehicle. For ArduPilot vehicles with no mission this caused a spurious 'unsaved changes' warning on app close. Only set dirtyForSave when the plan actually contains items. --- src/FirmwarePlugin/FirmwarePlugin.h | 2 +- src/MissionManager/PlanMasterController.cc | 2 +- src/Vehicle/Vehicle.cc | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/FirmwarePlugin/FirmwarePlugin.h b/src/FirmwarePlugin/FirmwarePlugin.h index 96997e4f3ae4..06334232d522 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.h +++ b/src/FirmwarePlugin/FirmwarePlugin.h @@ -207,7 +207,7 @@ class FirmwarePlugin : public QObject /// Command the vehicle to start the mission virtual void startMission(Vehicle *vehicle) const; - /// Command vehicle to move to specified location (altitude is included and relative) + /// Command vehicle to move to specified location (altitude is ignored, vehicle uses current altitude) /// @return true: goto command accepted, false: goto failed (vehicle not moved) virtual bool guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius = 0.0) const; diff --git a/src/MissionManager/PlanMasterController.cc b/src/MissionManager/PlanMasterController.cc index 427adf5280b2..16fb3de8ac26 100644 --- a/src/MissionManager/PlanMasterController.cc +++ b/src/MissionManager/PlanMasterController.cc @@ -261,7 +261,7 @@ void PlanMasterController::_loadGeoFenceComplete(void) void PlanMasterController::_loadRallyPointsComplete(void) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadRallyPointsComplete"; - _setDirtyStates(true /* dirtyForSave */, false /* dirtyForUpload */); + _setDirtyStates(containsItems() /* dirtyForSave */, false /* dirtyForUpload */); } void PlanMasterController::_sendMissionComplete(void) diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index beeee047aacf..21ada78bf884 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -2039,6 +2039,9 @@ bool Vehicle::guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double for if (!coordinate().isValid()) { return false; } + if (!gotoCoord.isValid()) { + return false; + } double maxDistance = SettingsManager::instance()->flyViewSettings()->maxGoToLocationDistance()->rawValue().toDouble(); if (coordinate().distanceTo(gotoCoord) > maxDistance) { qgcApp()->showAppMessage(QString("New location is too far. Must be less than %1 %2.").arg(qRound(FactMetaData::metersToAppSettingsHorizontalDistanceUnits(maxDistance).toDouble())).arg(FactMetaData::appSettingsHorizontalDistanceUnitsString())); From 3d87bd29ee6a815ffca8b331942950d5e87d30ed Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 13 Mar 2026 12:27:03 -0700 Subject: [PATCH 091/119] PlanMasterControllerTest: add download dirty-flag scenarios Add DownloadWithItemsDirtyForSave and DownloadEmptyNotDirtyForSave scenarios to the dirty-flags test matrix. These exercise _loadRallyPointsComplete and verify that dirtyForSave is only set when the downloaded plan contains items. Covers the bug fixed in #14128. --- .../PlanMasterControllerTest.cc | 27 ++++++++++++++++--- .../MissionManager/PlanMasterControllerTest.h | 2 ++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/test/MissionManager/PlanMasterControllerTest.cc b/test/MissionManager/PlanMasterControllerTest.cc index c1f74f16fc06..12cd9a941776 100644 --- a/test/MissionManager/PlanMasterControllerTest.cc +++ b/test/MissionManager/PlanMasterControllerTest.cc @@ -70,10 +70,10 @@ void PlanMasterControllerTest::_testDirtyFlagsMatrix_data() { // Dirty-state transition matrix ("unchanged" means preserve prior value): // - // | State \ Action | Upload OK | Clear | SaveDirty=true | Load plan | Save file OK | Clear save-dirty | - // |----------------|-----------|-------|----------------|-----------|--------------|------------------| - // | dirtyForSave | unchanged | false | true | false | false | false | - // | dirtyForUpload | false | false | true | true | unchanged | unchanged | + // | State \ Action | Upload OK | Clear | SaveDirty=true | Load plan | Save file OK | Clear save-dirty | Download w/ items | Download empty | + // |----------------|-----------|-------|----------------|-----------|--------------|------------------|-------------------|----------------| + // | dirtyForSave | unchanged | false | true | false | false | false | true | false | + // | dirtyForUpload | false | false | true | true | unchanged | unchanged | false | false | // Data columns: // - scenario: DirtyScenario enum value selecting which action path to execute @@ -106,6 +106,8 @@ void PlanMasterControllerTest::_testDirtyFlagsMatrix_data() { SaveFalseOnSuccessfulLoad, "save false on successful load", DirtyStateFalse, DirtyStateTrue }, { ClearSaveDirtyPreservesUploadTrue, "clear save dirty keeps upload true", DirtyStateFalse, DirtyStateUnchanged }, { ClearSaveDirtyPreservesUploadFalse, "clear save dirty keeps upload false", DirtyStateFalse, DirtyStateUnchanged }, + { DownloadWithItemsDirtyForSave, "download with items marks save dirty",DirtyStateTrue, DirtyStateFalse }, + { DownloadEmptyNotDirtyForSave, "download empty keeps save clean", DirtyStateFalse, DirtyStateFalse }, }; const QList initialStates = { @@ -151,6 +153,11 @@ void PlanMasterControllerTest::_testDirtyFlagsMatrix() QVERIFY(initialDirtyForSave != DirtyStateUnchanged); QVERIFY(initialDirtyForUpload != DirtyStateUnchanged); + // Pre-load items for scenarios that need containsItems() == true + if (scenario == DownloadWithItemsDirtyForSave) { + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + } + const auto dirtyStateToBool = [](int state) -> bool { switch (state) { case DirtyStateFalse: @@ -213,6 +220,18 @@ void PlanMasterControllerTest::_testDirtyFlagsMatrix() case ClearSaveDirtyPreservesUploadFalse: _masterController->_setDirtyForSave(false); break; + case DownloadWithItemsDirtyForSave: { + QVERIFY(_masterController->containsItems()); + const bool invoked = QMetaObject::invokeMethod(_masterController, "_loadRallyPointsComplete", Qt::DirectConnection); + QVERIFY(invoked); + break; + } + case DownloadEmptyNotDirtyForSave: { + QVERIFY(!_masterController->containsItems()); + const bool invoked = QMetaObject::invokeMethod(_masterController, "_loadRallyPointsComplete", Qt::DirectConnection); + QVERIFY(invoked); + break; + } } const auto resolveExpected = [](int expectedState, bool unchangedValue) -> bool { diff --git a/test/MissionManager/PlanMasterControllerTest.h b/test/MissionManager/PlanMasterControllerTest.h index 983247d24371..34f1cc1f0bf5 100644 --- a/test/MissionManager/PlanMasterControllerTest.h +++ b/test/MissionManager/PlanMasterControllerTest.h @@ -40,6 +40,8 @@ private slots: SaveFalseOnSuccessfulLoad, ClearSaveDirtyPreservesUploadTrue, ClearSaveDirtyPreservesUploadFalse, + DownloadWithItemsDirtyForSave, + DownloadEmptyNotDirtyForSave, }; enum DirtyState { From e1fa3f6e9ee257244b31b4faf34240dbc0c78126 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:55:17 -0400 Subject: [PATCH 092/119] chore: add daily CI workflow to sync PX4 metadata (#14127) Co-authored-by: mrpollo <317648+mrpollo@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .github/workflows/px4-metadata.yml | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/px4-metadata.yml diff --git a/.github/workflows/px4-metadata.yml b/.github/workflows/px4-metadata.yml new file mode 100644 index 000000000000..b1ed35e09e10 --- /dev/null +++ b/.github/workflows/px4-metadata.yml @@ -0,0 +1,49 @@ +name: Update PX4 Metadata + +on: + schedule: + - cron: '0 6 * * *' # Daily at 6:00 AM UTC + workflow_dispatch: + +concurrency: + group: 'px4-metadata' + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + update-px4-metadata: + if: github.repository_owner == 'mavlink' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@v6 + + - name: Download PX4 airframe metadata + run: curl -fsSL https://artifacts.px4.io/Firmware/_general/airframes.xml -o src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml + + - name: Download PX4 parameter metadata + run: curl -fsSL https://artifacts.px4.io/Firmware/_general/parameters.xml -o src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + branch: update-px4-metadata + title: "chore: update PX4 metadata" + commit-message: "chore: update PX4 metadata" + body: | + This PR was automatically generated by GitHub Actions to update the bundled PX4 metadata files. + + Updated files: + - `src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml` (airframe definitions) + - `src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml` (parameter catalog) + + Source: https://artifacts.px4.io/Firmware/_general/ From 9cef2d16ff6dd6f0dce2de0a9f948b0290dfacaf Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 13 Mar 2026 14:52:58 -0700 Subject: [PATCH 093/119] Move misplaced QML files back to their proper modules Move FirstRunPrompt.qml to FirstRunPromptDialogs module and OfflineMapEditor.qml/OfflineMapInfo.qml to AppSettings module. These files were parked in QmlControls due to CMake set_source_files_properties() directory scope limitations, but only ScreenTools.qml (singleton) actually requires that workaround. Also remove stale set_source_files_properties for OfflineMapEditor.qml from QtLocationPlugin/CMakeLists.txt. Fixes #12935 --- src/QmlControls/CMakeLists.txt | 19 +++++-------------- src/QtLocationPlugin/CMakeLists.txt | 2 -- src/UI/AppSettings/CMakeLists.txt | 3 +++ .../AppSettings}/OfflineMapEditor.qml | 0 .../AppSettings}/OfflineMapInfo.qml | 0 src/UI/FirstRunPromptDialogs/CMakeLists.txt | 1 + .../FirstRunPromptDialogs}/FirstRunPrompt.qml | 0 7 files changed, 9 insertions(+), 16 deletions(-) rename src/{QmlControls => UI/AppSettings}/OfflineMapEditor.qml (100%) rename src/{QmlControls => UI/AppSettings}/OfflineMapInfo.qml (100%) rename src/{QmlControls => UI/FirstRunPromptDialogs}/FirstRunPrompt.qml (100%) diff --git a/src/QmlControls/CMakeLists.txt b/src/QmlControls/CMakeLists.txt index 26b35c1deb4b..35fc27e749b7 100644 --- a/src/QmlControls/CMakeLists.txt +++ b/src/QmlControls/CMakeLists.txt @@ -179,20 +179,11 @@ qt_add_qml_module(QGroundControlControlsModule VehicleRotationCal.qml VehicleSummaryRow.qml -# NOTE: The QML files below belong to other modules but are declared here because -# CMake's set_source_files_properties() has directory scope. Properties like -# QT_QML_SINGLETON_TYPE must be visible in the same CMakeLists.txt that calls -# qt_add_qml_module(). On Windows this cross-directory property propagation fails, -# so these files are kept in this module until a better solution is available. - - # src/FirstRunPromptDialogs/ - FirstRunPrompt.qml - - # src/UI/preferences/ - OfflineMapInfo.qml - - # src/QtLocationPlugin/QMLControl/ - OfflineMapEditor.qml +# NOTE: ScreenTools.qml is declared here because CMake's +# set_source_files_properties() has directory scope. The QT_QML_SINGLETON_TYPE +# property must be visible in the same CMakeLists.txt that calls +# qt_add_qml_module(). On Windows this cross-directory property propagation +# fails, so this file is kept in this module until a better solution is available. ScreenTools.qml NO_PLUGIN diff --git a/src/QtLocationPlugin/CMakeLists.txt b/src/QtLocationPlugin/CMakeLists.txt index d403c768d929..83d57ff1d7a8 100644 --- a/src/QtLocationPlugin/CMakeLists.txt +++ b/src/QtLocationPlugin/CMakeLists.txt @@ -92,6 +92,4 @@ target_include_directories(QGCLocation Providers ) -set_source_files_properties(QMLControl/OfflineMapEditor.qml PROPERTIES QT_RESOURCE_ALIAS OfflineMapEditor.qml) - target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE QGCLocation) diff --git a/src/UI/AppSettings/CMakeLists.txt b/src/UI/AppSettings/CMakeLists.txt index 84eb16fa66a7..1883a2399d81 100644 --- a/src/UI/AppSettings/CMakeLists.txt +++ b/src/UI/AppSettings/CMakeLists.txt @@ -28,5 +28,8 @@ qt_add_qml_module(AppSettingsModule TelemetrySettings.qml UdpSettings.qml VideoSettings.qml + + OfflineMapEditor.qml + OfflineMapInfo.qml NO_PLUGIN ) diff --git a/src/QmlControls/OfflineMapEditor.qml b/src/UI/AppSettings/OfflineMapEditor.qml similarity index 100% rename from src/QmlControls/OfflineMapEditor.qml rename to src/UI/AppSettings/OfflineMapEditor.qml diff --git a/src/QmlControls/OfflineMapInfo.qml b/src/UI/AppSettings/OfflineMapInfo.qml similarity index 100% rename from src/QmlControls/OfflineMapInfo.qml rename to src/UI/AppSettings/OfflineMapInfo.qml diff --git a/src/UI/FirstRunPromptDialogs/CMakeLists.txt b/src/UI/FirstRunPromptDialogs/CMakeLists.txt index f2c89fc493c7..822b4bed3791 100644 --- a/src/UI/FirstRunPromptDialogs/CMakeLists.txt +++ b/src/UI/FirstRunPromptDialogs/CMakeLists.txt @@ -5,6 +5,7 @@ qt_add_qml_module(FirstRunPromptDialogsModule VERSION 1.0 RESOURCE_PREFIX /qml QML_FILES + FirstRunPrompt.qml OfflineVehicleFirstRunPrompt.qml UnitsFirstRunPrompt.qml NO_PLUGIN diff --git a/src/QmlControls/FirstRunPrompt.qml b/src/UI/FirstRunPromptDialogs/FirstRunPrompt.qml similarity index 100% rename from src/QmlControls/FirstRunPrompt.qml rename to src/UI/FirstRunPromptDialogs/FirstRunPrompt.qml From 45f282bf09d0f83766b964dd363a9caca90566f6 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Fri, 13 Mar 2026 17:38:40 -0700 Subject: [PATCH 094/119] Optimize polygon/polyline drag performance with dual-signal architecture Fixes #13011 - Add vertexDrag/dragPath/dragCenter properties to QGCMapPolygon and QGCMapPolyline that emit lightweight dragPathChanged signals during interactive drag instead of pathChanged, avoiding expensive downstream recalculations (transect rebuilds, terrain queries) until drag ends. - Hide corridor scan polygon and transect visuals during polyline drag. - Orange border and edge length labels shown during any drag operation. - Hide split/vertex/center handles as appropriate during drags. - Circle mode: hide radius handle during center drag, radius drag participates in vertexDrag state, radius handle tracks mouse during debounced polygon rebuilds, center handle click opens context menu, Set Radius dialog implemented with unit conversion. --- .../MapItems/QGCMapPolygonVisuals.qml | 103 +++++++++++++++--- .../MapItems/QGCMapPolylineVisuals.qml | 78 ++++++++++++- src/PlanView/CorridorScanMapVisual.qml | 1 + src/PlanView/TransectStyleMapVisuals.qml | 21 ++-- src/QmlControls/QGCMapPolygon.cc | 84 ++++++++++---- src/QmlControls/QGCMapPolygon.h | 9 ++ src/QmlControls/QGCMapPolyline.cc | 28 ++++- src/QmlControls/QGCMapPolyline.h | 7 ++ 8 files changed, 274 insertions(+), 57 deletions(-) diff --git a/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml b/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml index 8888a72181de..55c93e00a855 100644 --- a/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml +++ b/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml @@ -27,7 +27,6 @@ Item { property real _circleRadius property bool _circleRadiusDrag: false property var _circleRadiusDragCoord: QtPositioning.coordinate() - property bool _editCircleRadius: false property string _instructionText: _polygonToolsText property var _savedVertices: [ ] property bool _savedCircleMode @@ -252,7 +251,7 @@ Item { QGCMenuItem { text: qsTr("Set radius..." ) visible: _circleMode - onTriggered: _editCircleRadius = true + onTriggered: editCircleRadiusDialogFactory.open() } QGCMenuItem { @@ -274,9 +273,10 @@ Item { MapPolygon { color: mapPolygon.showAltColor ? altColor : interiorColor opacity: interiorOpacity - border.color: borderColor - border.width: borderWidth - path: mapPolygon.path + visible: _root.visible + border.color: mapPolygon.vertexDrag ? "orange" : borderColor + border.width: mapPolygon.vertexDrag ? 3 : borderWidth + path: mapPolygon.vertexDrag ? mapPolygon.dragPath : mapPolygon.path } } @@ -310,19 +310,24 @@ Item { delegate: Item { property var _edgeLengthHandle - property var _vertices: mapPolygon.path function _setHandlePosition() { + var vertices = mapPolygon.vertexDrag ? mapPolygon.dragPath : mapPolygon.path var nextIndex = index + 1 - if (nextIndex > _vertices.length - 1) { + if (nextIndex > vertices.length - 1) { nextIndex = 0 } - var distance = _vertices[index].distanceTo(_vertices[nextIndex]) - var azimuth = _vertices[index].azimuthTo(_vertices[nextIndex]) - _edgeLengthHandle.coordinate =_vertices[index].atDistanceAndAzimuth(distance / 3, azimuth) + var distance = vertices[index].distanceTo(vertices[nextIndex]) + var azimuth = vertices[index].azimuthTo(vertices[nextIndex]) + _edgeLengthHandle.coordinate = vertices[index].atDistanceAndAzimuth(distance / 2, azimuth) _edgeLengthHandle.distance = distance } + Connections { + target: mapPolygon + function onDragPathChanged() { _setHandlePosition() } + } + Component.onCompleted: { _edgeLengthHandle = edgeLengthHandleComponent.createObject(mapControl) _edgeLengthHandle.vertexIndex = index @@ -346,7 +351,7 @@ Item { id: mapQuickItem anchorPoint.x: sourceItem.width / 2 anchorPoint.y: sourceItem.height / 2 - visible: !_circleMode + visible: !_circleMode && !_isVertexBeingDragged && !mapPolygon.centerDrag property int vertexIndex @@ -402,8 +407,8 @@ Item { mapControl: _root.mapControl z: _zorderDragHandle visible: !_circleMode - onDragStart: _isVertexBeingDragged = true - onDragStop: { _isVertexBeingDragged = false; mapPolygon.verifyClockwiseWinding() } + onDragStart: { _isVertexBeingDragged = true; mapPolygon.vertexDrag = true } + onDragStop: { _isVertexBeingDragged = false; mapPolygon.vertexDrag = false; mapPolygon.verifyClockwiseWinding() } property int polygonVertex @@ -429,6 +434,7 @@ Item { anchorPoint.x: dragHandle.width * 0.5 anchorPoint.y: dragHandle.height * 0.5 z: _zorderDragHandle + visible: !_isVertexBeingDragged sourceItem: Rectangle { id: dragHandle width: ScreenTools.defaultFontPixelHeight * 1.5 @@ -459,7 +465,7 @@ Item { anchorPoint.x: dragHandle.width / 2 anchorPoint.y: dragHandle.height / 2 z: _zorderDragHandle - visible: !_circleMode + visible: !_circleMode && !mapPolygon.centerDrag property int polygonVertex @@ -506,6 +512,56 @@ Item { } } + QGCPopupDialogFactory { + id: editCircleRadiusDialogFactory + + dialogComponent: editCircleRadiusDialog + } + + Component { + id: editCircleRadiusDialog + + QGCPopupDialog { + id: popupDialog + title: qsTr("Set Radius") + buttons: Dialog.Save | Dialog.Cancel + + onAccepted: { + const appRadius = Number(radiusField.text) + if (!isNaN(appRadius) && appRadius > 0) { + const radiusMeters = QGroundControl.unitsConversion.appSettingsHorizontalDistanceUnitsToMeters(appRadius) + _createCircularPolygon(mapPolygon.center, radiusMeters) + } else { + preventClose = true + } + } + + ColumnLayout { + width: ScreenTools.defaultFontPixelWidth * 30 + spacing: ScreenTools.defaultFontPixelHeight + + QGCLabel { + Layout.fillWidth: true + text: qsTr("Enter circle radius.") + wrapMode: Text.WordWrap + } + + QGCTextField { + id: radiusField + Layout.fillWidth: true + text: QGroundControl.unitsConversion.metersToAppSettingsHorizontalDistanceUnits(_circleRadius).toFixed(1) + validator: DoubleValidator { bottom: 0.1; notation: DoubleValidator.StandardNotation } + inputMethodHints: Qt.ImhFormattedNumbersOnly + } + + QGCLabel { + Layout.fillWidth: true + text: QGroundControl.unitsConversion.appSettingsHorizontalDistanceUnitsString + } + } + } + } + QGCPopupDialogFactory { id: editCenterPositionDialogFactory @@ -554,8 +610,9 @@ Item { mapControl: _root.mapControl z: _zorderCenterHandle onItemCoordinateChanged: mapPolygon.center = itemCoordinate - onDragStart: mapPolygon.centerDrag = true - onDragStop: mapPolygon.centerDrag = false + onDragStart: { mapPolygon.centerDrag = true; mapPolygon.vertexDrag = true } + onDragStop: { mapPolygon.centerDrag = false; mapPolygon.vertexDrag = false } + onClicked: if(_root.interactive) menu.popupCenter() } } @@ -568,7 +625,7 @@ Item { Component.onCompleted: { dragHandle = centerDragHandle.createObject(mapControl) - dragHandle.coordinate = Qt.binding(function() { return mapPolygon.center }) + dragHandle.coordinate = Qt.binding(function() { return mapPolygon.centerDrag ? mapPolygon.dragCenter : mapPolygon.center }) mapControl.addMapItem(dragHandle) dragArea = centerDragAreaComponent.createObject(mapControl, { "itemIndicator": dragHandle, "itemCoordinate": mapPolygon.center }) } @@ -655,6 +712,7 @@ Item { anchorPoint.x: dragHandle.width / 2 anchorPoint.y: dragHandle.height / 2 z: QGroundControl.zOrderMapItems + 2 + visible: !mapPolygon.centerDrag sourceItem: Rectangle { id: dragHandle @@ -690,8 +748,19 @@ Item { MissionItemIndicatorDrag { mapControl: _root.mapControl + onDragStart: { + _circleRadiusDrag = true + mapPolygon.vertexDrag = true + } + onDragStop: { + _circleRadiusDrag = false + mapPolygon.vertexDrag = false + } onItemCoordinateChanged: { + // Keep the handle visually attached to the cursor while radius updates are de-bounced. + _circleRadiusDragCoord = itemCoordinate + var radius = mapPolygon.center.distanceTo(itemCoordinate) if (Math.abs(radius - _circleRadius) > 0.1) { diff --git a/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml b/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml index 43e2df894be1..7238024172cd 100644 --- a/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml +++ b/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml @@ -25,6 +25,8 @@ Item { property real _zorderDragHandle: QGroundControl.zOrderMapItems + 3 // Highest to prevent splitting when items overlap property real _zorderSplitHandle: QGroundControl.zOrderMapItems + 2 property var _savedVertices: [ ] + property bool _isVertexBeingDragged: false + property bool dragging: _isVertexBeingDragged readonly property string _corridorToolsText: qsTr("Polyline Tools") readonly property string _traceText: qsTr("Click in the map to add vertices. Click 'Done Tracing' when finished.") @@ -37,7 +39,7 @@ Item { function _addInteractiveVisuals() { if (_objMgrInteractiveVisuals.empty) { - _objMgrInteractiveVisuals.createObjects([ dragHandlesComponent, splitHandlesComponent, toolbarComponent ], mapControl) + _objMgrInteractiveVisuals.createObjects([ dragHandlesComponent, splitHandlesComponent, edgeLengthHandlesComponent, toolbarComponent ], mapControl) } } @@ -162,9 +164,9 @@ Item { id: polylineComponent MapPolyline { - line.width: lineWidth - line.color: lineColor - path: mapPolyline.path + line.width: mapPolyline.vertexDrag ? 3 : lineWidth + line.color: mapPolyline.vertexDrag ? "orange" : lineColor + path: mapPolyline.vertexDrag ? mapPolyline.dragPath : mapPolyline.path visible: _root.visible opacity: _root.opacity } @@ -179,6 +181,7 @@ Item { anchorPoint.y: sourceItem.height / 2 z: _zorderSplitHandle opacity: _root.opacity + visible: !mapPolyline.vertexDrag property int vertexIndex @@ -225,6 +228,71 @@ Item { } } + Component { + id: edgeLengthHandleComponent + + MapQuickItem { + id: edgeLengthMapItem + anchorPoint.x: sourceItem.width / 2 + anchorPoint.y: sourceItem.height / 2 + + property int vertexIndex + property real distance + + property var _unitsConversion: QGroundControl.unitsConversion + + sourceItem: Text { + text: _unitsConversion.metersToAppSettingsHorizontalDistanceUnits(distance).toFixed(1) + " " + + _unitsConversion.appSettingsHorizontalDistanceUnitsString + color: "white" + } + } + } + + Component { + id: edgeLengthHandlesComponent + + Repeater { + model: _isVertexBeingDragged ? mapPolyline.path : undefined + + delegate: Item { + property var _edgeLengthHandle + + function _setHandlePosition() { + var vertices = mapPolyline.vertexDrag ? mapPolyline.dragPath : mapPolyline.path + var nextIndex = index + 1 + if (nextIndex > vertices.length - 1) { + return + } + var distance = vertices[index].distanceTo(vertices[nextIndex]) + var azimuth = vertices[index].azimuthTo(vertices[nextIndex]) + _edgeLengthHandle.coordinate = vertices[index].atDistanceAndAzimuth(distance / 2, azimuth) + _edgeLengthHandle.distance = distance + } + + Connections { + target: mapPolyline + function onDragPathChanged() { _setHandlePosition() } + } + + Component.onCompleted: { + if (index + 1 <= mapPolyline.path.length - 1) { + _edgeLengthHandle = edgeLengthHandleComponent.createObject(mapControl) + _edgeLengthHandle.vertexIndex = index + _setHandlePosition() + mapControl.addMapItem(_edgeLengthHandle) + } + } + + Component.onDestruction: { + if (_edgeLengthHandle) { + _edgeLengthHandle.destroy() + } + } + } + } + } + // Control which is used to drag polygon vertices Component { id: dragAreaComponent @@ -234,6 +302,8 @@ Item { id: dragArea z: _zorderDragHandle opacity: _root.opacity + onDragStart: { _isVertexBeingDragged = true; mapPolyline.vertexDrag = true } + onDragStop: { _isVertexBeingDragged = false; mapPolyline.vertexDrag = false } property int polylineVertex diff --git a/src/PlanView/CorridorScanMapVisual.qml b/src/PlanView/CorridorScanMapVisual.qml index ce5a82c8351c..0c32d1d15717 100644 --- a/src/PlanView/CorridorScanMapVisual.qml +++ b/src/PlanView/CorridorScanMapVisual.qml @@ -10,6 +10,7 @@ import QGroundControl.FlightMap /// Corridor Scan Complex Mission Item visuals TransectStyleMapVisuals { polygonInteractive: false + hideMapPolygon: mapPolylineVisuals.dragging property bool _currentItem: object.isCurrentItem diff --git a/src/PlanView/TransectStyleMapVisuals.qml b/src/PlanView/TransectStyleMapVisuals.qml index b987ce079ef3..ce6ddf17da54 100644 --- a/src/PlanView/TransectStyleMapVisuals.qml +++ b/src/PlanView/TransectStyleMapVisuals.qml @@ -15,10 +15,12 @@ Item { property bool polygonInteractive: true property bool interactive: true property var vehicle + property bool hideMapPolygon: false property var _missionItem: object property var _mapPolygon: object.surveyAreaPolygon property bool _currentItem: object.isCurrentItem + property bool _vertexDrag: _mapPolygon.vertexDrag || hideMapPolygon property var _transectPoints: _missionItem.visualTransectPoints property int _transectCount: _transectPoints.length / (_hasTurnaround ? 4 : 2) property bool _hasTurnaround: _missionItem.turnAroundDistance.rawValue !== 0 @@ -67,6 +69,7 @@ Item { interiorColor: QGroundControl.globalPalette.surveyPolygonInterior altColor: QGroundControl.globalPalette.surveyPolygonTerrainCollision interiorOpacity: 0.5 * _root.opacity + visible: !hideMapPolygon } // Full set of transects lines. Shown when item is selected. @@ -77,7 +80,7 @@ Item { line.color: "white" line.width: 2 path: _transectPoints - visible: _currentItem + visible: _currentItem && !_vertexDrag opacity: _root.opacity } } @@ -90,7 +93,7 @@ Item { line.color: "white" line.width: 2 path: _showPartialEntryExit ? [ _transectPoints[0], _transectPoints[1] ] : [] - visible: _showPartialEntryExit + visible: _showPartialEntryExit && !_vertexDrag opacity: _root.opacity } } @@ -101,7 +104,7 @@ Item { line.color: "white" line.width: 2 path: _showPartialEntryExit ? [ _transectPoints[_lastPointIndex - 1], _transectPoints[_lastPointIndex] ] : [] - visible: _showPartialEntryExit + visible: _showPartialEntryExit && !_vertexDrag opacity: _root.opacity } } @@ -115,7 +118,7 @@ Item { anchorPoint.y: sourceItem.anchorPointY z: QGroundControl.zOrderMapItems coordinate: _missionItem.coordinate - visible: _missionItem.exitCoordinate.isValid + visible: _missionItem.exitCoordinate.isValid && !_vertexDrag opacity: _root.opacity sourceItem: MissionItemIndexLabel { @@ -133,7 +136,7 @@ Item { fromCoord: _transectPoints[_firstTrueTransectIndex] toCoord: _transectPoints[_firstTrueTransectIndex + 1] arrowPosition: 1 - visible: _currentItem + visible: _currentItem && !_vertexDrag opacity: _root.opacity } } @@ -145,7 +148,7 @@ Item { fromCoord: _transectPoints[nextTrueTransectIndex] toCoord: _transectPoints[nextTrueTransectIndex + 1] arrowPosition: 1 - visible: _currentItem && _transectCount > 3 + visible: _currentItem && _transectCount > 3 && !_vertexDrag opacity: _root.opacity property int nextTrueTransectIndex: _firstTrueTransectIndex + (_hasTurnaround ? 4 : 2) @@ -159,7 +162,7 @@ Item { fromCoord: _transectPoints[_lastTrueTransectIndex - 1] toCoord: _transectPoints[_lastTrueTransectIndex] arrowPosition: 3 - visible: _currentItem + visible: _currentItem && !_vertexDrag opacity: _root.opacity } } @@ -171,7 +174,7 @@ Item { fromCoord: _transectPoints[prevTrueTransectIndex - 1] toCoord: _transectPoints[prevTrueTransectIndex] arrowPosition: 13 - visible: _currentItem && _transectCount > 3 + visible: _currentItem && _transectCount > 3 && !_vertexDrag opacity: _root.opacity property int prevTrueTransectIndex: _lastTrueTransectIndex - (_hasTurnaround ? 4 : 2) @@ -187,7 +190,7 @@ Item { anchorPoint.y: sourceItem.anchorPointY z: QGroundControl.zOrderMapItems coordinate: _missionItem.exitCoordinate - visible: _missionItem.exitCoordinate.isValid + visible: _missionItem.exitCoordinate.isValid && !_vertexDrag opacity: _root.opacity sourceItem: MissionItemIndexLabel { diff --git a/src/QmlControls/QGCMapPolygon.cc b/src/QmlControls/QGCMapPolygon.cc index 235320ff283c..a9ed418b8233 100644 --- a/src/QmlControls/QGCMapPolygon.cc +++ b/src/QmlControls/QGCMapPolygon.cc @@ -77,7 +77,11 @@ void QGCMapPolygon::clear(void) while (_polygonPath.count() > 1) { _polygonPath.takeLast(); } - emit pathChanged(); + if (_vertexDrag) { + emit dragPathChanged(); + } else { + emit pathChanged(); + } // Although this code should remove the polygon from the map it doesn't. There appears // to be a bug in QGCMapPolygon which causes it to not be redrawn if the list is empty. So @@ -97,14 +101,20 @@ void QGCMapPolygon::adjustVertex(int vertexIndex, const QGeoCoordinate coordinat _polygonPath[vertexIndex] = QVariant::fromValue(coordinate); _polygonModel.value(vertexIndex)->setCoordinate(coordinate); if (!_centerDrag) { - // When dragging center we don't signal path changed until all vertices are updated if (!_deferredPathChanged) { - // Only update the path once per event loop, to prevent lag-spikes _deferredPathChanged = true; - QTimer::singleShot(0, this, [this]() { - emit pathChanged(); - _deferredPathChanged = false; - }); + if (_vertexDrag) { + // During vertex drag only emit the lightweight visual signal + QTimer::singleShot(0, this, [this]() { + emit dragPathChanged(); + _deferredPathChanged = false; + }); + } else { + QTimer::singleShot(0, this, [this]() { + emit pathChanged(); + _deferredPathChanged = false; + }); + } } } setDirty(true); @@ -275,7 +285,11 @@ void QGCMapPolygon::appendVertex(const QGeoCoordinate& coordinate) // Only update the path once per event loop, to prevent lag-spikes _deferredPathChanged = true; QTimer::singleShot(0, this, [this]() { - emit pathChanged(); + if (_vertexDrag) { + emit dragPathChanged(); + } else { + emit pathChanged(); + } _deferredPathChanged = false; }); } @@ -293,7 +307,11 @@ void QGCMapPolygon::appendVertices(const QList& coordinates) _polygonModel.append(objects); endReset(); - emit pathChanged(); + if (_vertexDrag) { + emit dragPathChanged(); + } else { + emit pathChanged(); + } } void QGCMapPolygon::appendVertices(const QVariantList& varCoords) @@ -376,29 +394,29 @@ void QGCMapPolygon::setCenter(QGeoCoordinate newCenter) adjustVertex(i, newVertex); } + _ignoreCenterUpdates = false; + _center = newCenter; + if (_centerDrag) { - // When center dragging, signals from adjustVertext are not sent. So we need to signal here when all adjusting is complete. + // During center drag emit lightweight visual signals only + if (!_deferredPathChanged) { + _deferredPathChanged = true; + QTimer::singleShot(0, this, [this]() { + emit dragPathChanged(); + emit dragCenterChanged(_center); + _deferredPathChanged = false; + }); + } + } else { if (!_deferredPathChanged) { - // Only update the path once per event loop, to prevent lag-spikes _deferredPathChanged = true; QTimer::singleShot(0, this, [this]() { emit pathChanged(); + emit centerChanged(_center); _deferredPathChanged = false; }); } } - - _ignoreCenterUpdates = false; - - _center = newCenter; - if (!_deferredPathChanged) { - // Only update the center once per event loop, to prevent lag-spikes - _deferredPathChanged = true; - QTimer::singleShot(0, this, [this, newCenter]() { - emit centerChanged(newCenter); - _deferredPathChanged = false; - }); - } } } @@ -406,10 +424,30 @@ void QGCMapPolygon::setCenterDrag(bool centerDrag) { if (centerDrag != _centerDrag) { _centerDrag = centerDrag; + if (!centerDrag) { + // Drag ended. Center edits do not use vertexDrag, so emit pathChanged here. + // For interactive center drag, setVertexDrag(false) emits pathChanged. + if (!_vertexDrag) { + emit pathChanged(); + } + emit centerChanged(_center); + } emit centerDragChanged(centerDrag); } } +void QGCMapPolygon::setVertexDrag(bool vertexDrag) +{ + if (_vertexDrag != vertexDrag) { + _vertexDrag = vertexDrag; + if (!vertexDrag) { + // Drag ended - signal path changed so downstream can recalculate + emit pathChanged(); + } + emit vertexDragChanged(vertexDrag); + } +} + void QGCMapPolygon::setInteractive(bool interactive) { if (_interactive != interactive) { diff --git a/src/QmlControls/QGCMapPolygon.h b/src/QmlControls/QGCMapPolygon.h index 2037222b4ccf..7a3c58de832f 100644 --- a/src/QmlControls/QGCMapPolygon.h +++ b/src/QmlControls/QGCMapPolygon.h @@ -27,11 +27,14 @@ class QGCMapPolygon : public QObject Q_PROPERTY(int count READ count NOTIFY countChanged) Q_PROPERTY(QVariantList path READ path NOTIFY pathChanged) + Q_PROPERTY(QVariantList dragPath READ path NOTIFY dragPathChanged) Q_PROPERTY(double area READ area NOTIFY pathChanged) Q_PROPERTY(QmlObjectListModel* pathModel READ qmlPathModel CONSTANT) Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) Q_PROPERTY(QGeoCoordinate center READ center WRITE setCenter NOTIFY centerChanged) + Q_PROPERTY(QGeoCoordinate dragCenter READ center NOTIFY dragCenterChanged) Q_PROPERTY(bool centerDrag READ centerDrag WRITE setCenterDrag NOTIFY centerDragChanged) + Q_PROPERTY(bool vertexDrag READ vertexDrag WRITE setVertexDrag NOTIFY vertexDragChanged) Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged) Q_PROPERTY(bool empty READ empty NOTIFY isEmptyChanged) @@ -102,6 +105,7 @@ class QGCMapPolygon : public QObject void setDirty (bool dirty); QGeoCoordinate center (void) const { return _center; } bool centerDrag (void) const { return _centerDrag; } + bool vertexDrag (void) const { return _vertexDrag; } bool interactive (void) const { return _interactive; } bool isValid (void) const { return _polygonModel.count() >= 3; } bool empty (void) const { return _polygonModel.count() == 0; } @@ -117,6 +121,7 @@ class QGCMapPolygon : public QObject void setPath (const QVariantList& path); void setCenter (QGeoCoordinate newCenter); void setCenterDrag (bool centerDrag); + void setVertexDrag (bool vertexDrag); void setInteractive (bool interactive); void setTraceMode (bool traceMode); void setShowAltColor(bool showAltColor); @@ -127,10 +132,13 @@ class QGCMapPolygon : public QObject signals: void countChanged (int count); void pathChanged (void); + void dragPathChanged (void); void dirtyChanged (bool dirty); void cleared (void); void centerChanged (QGeoCoordinate center); + void dragCenterChanged (QGeoCoordinate center); void centerDragChanged (bool centerDrag); + void vertexDragChanged (bool vertexDrag); void interactiveChanged (bool interactive); bool isValidChanged (void); bool isEmptyChanged (void); @@ -154,6 +162,7 @@ private slots: bool _dirty = false; QGeoCoordinate _center; bool _centerDrag = false; + bool _vertexDrag = false; bool _ignoreCenterUpdates = false; bool _interactive = false; bool _traceMode = false; diff --git a/src/QmlControls/QGCMapPolyline.cc b/src/QmlControls/QGCMapPolyline.cc index d91db82d0bef..1d3ee9b0a352 100644 --- a/src/QmlControls/QGCMapPolyline.cc +++ b/src/QmlControls/QGCMapPolyline.cc @@ -79,10 +79,18 @@ void QGCMapPolyline::adjustVertex(int vertexIndex, const QGeoCoordinate coordina _polylineModel.value(vertexIndex)->setCoordinate(coordinate); if (!_deferredPathChanged) { _deferredPathChanged = true; - QTimer::singleShot(0, this, [this]() { - emit pathChanged(); - _deferredPathChanged = false; - }); + if (_vertexDrag) { + // During vertex drag only emit the lightweight visual signal + QTimer::singleShot(0, this, [this]() { + emit dragPathChanged(); + _deferredPathChanged = false; + }); + } else { + QTimer::singleShot(0, this, [this]() { + emit pathChanged(); + _deferredPathChanged = false; + }); + } } setDirty(true); } @@ -262,6 +270,18 @@ void QGCMapPolyline::setInteractive(bool interactive) } } +void QGCMapPolyline::setVertexDrag(bool vertexDrag) +{ + if (_vertexDrag != vertexDrag) { + _vertexDrag = vertexDrag; + if (!vertexDrag) { + // Drag ended - signal path changed so downstream can recalculate + emit pathChanged(); + } + emit vertexDragChanged(vertexDrag); + } +} + QGeoCoordinate QGCMapPolyline::vertexCoordinate(int vertex) const { if (vertex >= 0 && vertex < _polylinePath.count()) { diff --git a/src/QmlControls/QGCMapPolyline.h b/src/QmlControls/QGCMapPolyline.h index 0068a0f81447..9b1974f89a01 100644 --- a/src/QmlControls/QGCMapPolyline.h +++ b/src/QmlControls/QGCMapPolyline.h @@ -20,9 +20,11 @@ class QGCMapPolyline : public QObject Q_PROPERTY(int count READ count NOTIFY countChanged) Q_PROPERTY(QVariantList path READ path NOTIFY pathChanged) + Q_PROPERTY(QVariantList dragPath READ path NOTIFY dragPathChanged) Q_PROPERTY(QmlObjectListModel* pathModel READ qmlPathModel CONSTANT) Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) + Q_PROPERTY(bool vertexDrag READ vertexDrag WRITE setVertexDrag NOTIFY vertexDragChanged) Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged) Q_PROPERTY(bool empty READ empty NOTIFY isEmptyChanged) Q_PROPERTY(bool traceMode READ traceMode WRITE setTraceMode NOTIFY traceModeChanged) @@ -80,6 +82,7 @@ class QGCMapPolyline : public QObject bool dirty (void) const { return _dirty; } void setDirty (bool dirty); bool interactive (void) const { return _interactive; } + bool vertexDrag (void) const { return _vertexDrag; } QVariantList path (void) const { return _polylinePath; } bool isValid (void) const { return _polylineModel.count() >= 2; } bool empty (void) const { return _polylineModel.count() == 0; } @@ -92,6 +95,7 @@ class QGCMapPolyline : public QObject void setPath (const QList& path); void setPath (const QVariantList& path); void setInteractive (bool interactive); + void setVertexDrag (bool vertexDrag); void setTraceMode (bool traceMode); void selectVertex (int index); @@ -100,9 +104,11 @@ class QGCMapPolyline : public QObject signals: void countChanged (int count); void pathChanged (void); + void dragPathChanged (void); void dirtyChanged (bool dirty); void cleared (void); void interactiveChanged (bool interactive); + void vertexDragChanged (bool vertexDrag); void isValidChanged (void); void isEmptyChanged (void); void traceModeChanged (bool traceMode); @@ -122,6 +128,7 @@ private slots: bool _deferredPathChanged = false; bool _dirty; bool _interactive; + bool _vertexDrag = false; bool _traceMode = false; int _selectedVertexIndex = -1; }; From 29f93f84b7ea639e4d89bf01a7ffcab62447a1f9 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 11:39:25 -0700 Subject: [PATCH 095/119] QGCMapPolygon: use surveyor's formula for centroid calculation Replace the vertex-average center calculation with the proper area centroid using the surveyor's (shoelace) formula. This gives the true geometric centroid weighted by area rather than a simple vertex mean, which is more accurate for irregular polygons. Falls back to vertex averaging for degenerate (zero-area) polygons. Replaces #13833. --- src/QmlControls/QGCMapPolygon.cc | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/QmlControls/QGCMapPolygon.cc b/src/QmlControls/QGCMapPolygon.cc index a9ed418b8233..f309f62390dc 100644 --- a/src/QmlControls/QGCMapPolygon.cc +++ b/src/QmlControls/QGCMapPolygon.cc @@ -365,12 +365,35 @@ void QGCMapPolygon::_updateCenter(void) QGeoCoordinate center; if (_polygonPath.count() > 2) { - QPointF centroid(0, 0); QPolygonF polygonF = _toPolygonF(); - for (int i=0; i Date: Sat, 14 Mar 2026 11:58:46 -0700 Subject: [PATCH 096/119] QGCMapPolygonTest: add centroid calculation tests - Rectangle: verifies center is at geometric center - Extra vertex: midpoint on one side doesn't shift area centroid - Degenerate: collinear points fall back to vertex average --- test/MissionManager/QGCMapPolygonTest.cc | 59 ++++++++++++++++++++++++ test/MissionManager/QGCMapPolygonTest.h | 3 ++ 2 files changed, 62 insertions(+) diff --git a/test/MissionManager/QGCMapPolygonTest.cc b/test/MissionManager/QGCMapPolygonTest.cc index 038c291b2559..fef01ac80d6b 100644 --- a/test/MissionManager/QGCMapPolygonTest.cc +++ b/test/MissionManager/QGCMapPolygonTest.cc @@ -252,6 +252,65 @@ void QGCMapPolygonTest::_testSegmentSplit() QVERIFY(_mapPolygon->selectedVertex() == _mapPolygon->count() - 2); } +void QGCMapPolygonTest::_testCenterRectangle() +{ + // Simple rectangle — centroid should be the geometric center + const QGeoCoordinate topLeft(47.636, -122.093); + const QGeoCoordinate topRight(47.636, -122.085); + const QGeoCoordinate bottomRight(47.630, -122.085); + const QGeoCoordinate bottomLeft(47.630, -122.093); + + _mapPolygon->appendVertices({topLeft, topRight, bottomRight, bottomLeft}); + QCoreApplication::processEvents(); + + const QGeoCoordinate expectedCenter(47.633, -122.089); + const QGeoCoordinate center = _mapPolygon->center(); + + QVERIFY(center.isValid()); + QCOMPARE_COORDS(center, expectedCenter); +} + +void QGCMapPolygonTest::_testCenterExtraVertex() +{ + // Rectangle with an extra vertex at the midpoint of one side. + // The shape is identical to the rectangle so the area centroid should be the same, + // but a naive vertex average would shift toward the side with the extra vertex. + const QGeoCoordinate topLeft(47.636, -122.093); + const QGeoCoordinate topMid(47.636, -122.089); // midpoint of top edge + const QGeoCoordinate topRight(47.636, -122.085); + const QGeoCoordinate bottomRight(47.630, -122.085); + const QGeoCoordinate bottomLeft(47.630, -122.093); + + _mapPolygon->appendVertices({topLeft, topMid, topRight, bottomRight, bottomLeft}); + QCoreApplication::processEvents(); + + const QGeoCoordinate expectedCenter(47.633, -122.089); + const QGeoCoordinate center = _mapPolygon->center(); + + QVERIFY(center.isValid()); + QCOMPARE_COORDS(center, expectedCenter); +} + +void QGCMapPolygonTest::_testCenterDegenerate() +{ + // Three collinear points — zero area polygon. + // The surveyor's formula produces zero area, so the fallback vertex average should be used. + const QGeoCoordinate left(47.633, -122.093); + const QGeoCoordinate middle(47.633, -122.089); + const QGeoCoordinate right(47.633, -122.085); + + _mapPolygon->appendVertices({left, middle, right}); + QCoreApplication::processEvents(); + + const QGeoCoordinate center = _mapPolygon->center(); + + const QGeoCoordinate expectedCenter(47.633, -122.089); + + QVERIFY(center.isValid()); + // Vertex average: latitude stays 47.633, longitude = mean of the three + QCOMPARE_COORDS(center, expectedCenter); +} + #include "UnitTest.h" UT_REGISTER_TEST(QGCMapPolygonTest, TestLabel::Unit, TestLabel::MissionManager) diff --git a/test/MissionManager/QGCMapPolygonTest.h b/test/MissionManager/QGCMapPolygonTest.h index 10fcc6295a3a..eed49f856bef 100644 --- a/test/MissionManager/QGCMapPolygonTest.h +++ b/test/MissionManager/QGCMapPolygonTest.h @@ -23,6 +23,9 @@ private slots: void _testKMLLoad(); void _testSelectVertex(); void _testSegmentSplit(); + void _testCenterRectangle(); + void _testCenterExtraVertex(); + void _testCenterDegenerate(); private: MultiSignalSpy* _multiSpyPolygon = nullptr; From ca791dc4c8f485f94a1c90d2022758698159718a Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 12:51:28 -0700 Subject: [PATCH 097/119] PX4: Power summary show all batteries - Use Repeater/Loader pattern to display summary rows for all indexed batteries instead of only the first one - Battery index shown in labels only when multiple batteries are present - All labels are complete qsTr() strings for proper localization - Add BAT2 parameters to MockLink for testing multi-battery scenarios Fixes #13069 --- .../PX4/PowerComponentSummary.qml | 66 ++++++++++++++----- src/Comms/MockLink/PX4MockLink.params | 9 +++ 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml b/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml index c5ace0108e26..08b7e2751f4d 100644 --- a/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml @@ -15,34 +15,64 @@ Item { FactPanelController { id: controller; } - BatteryParams { - id: battParams - controller: controller - batteryIndex: 1 + property int _indexedBatteryParamCount: { + var batteryIndex = 1 + while (controller.parameterExists(-1, "BAT" + batteryIndex + "_SOURCE")) { + batteryIndex++ + } + return batteryIndex - 1 } ColumnLayout { id: mainLayout spacing: 0 - VehicleSummaryRow { - labelText: qsTr("Battery Source") - valueText: battParams.battSource.enumStringValue - } + Repeater { + model: _indexedBatteryParamCount - VehicleSummaryRow { - labelText: qsTr("Battery Full") - valueText: battParams.battHighVoltAvailable ? battParams.battHighVolt.valueString + " " + battParams.battHighVolt.units : _naString - } + Loader { + sourceComponent: batterySummaryComponent - VehicleSummaryRow { - labelText: qsTr("Battery Empty") - valueText: battParams.battLowVoltAvailable ? battParams.battLowVolt.valueString + " " + battParams.battLowVolt.units : _naString + property int batteryIndex: index + 1 + property bool showBatteryIndex: _indexedBatteryParamCount > 1 + } } + } + + Component { + id: batterySummaryComponent + + ColumnLayout { + spacing: 0 + + property var _controller: controller + property int _batteryIndex: batteryIndex + + BatteryParams { + id: battParams + controller: _controller + batteryIndex: _batteryIndex + } + + VehicleSummaryRow { + labelText: showBatteryIndex ? qsTr("Battery %1 Source").arg(batteryIndex) : qsTr("Battery Source") + valueText: battParams.battSource.enumStringValue + } + + VehicleSummaryRow { + labelText: showBatteryIndex ? qsTr("Battery %1 Full").arg(batteryIndex) : qsTr("Battery Full") + valueText: battParams.battHighVoltAvailable ? battParams.battHighVolt.valueString + " " + battParams.battHighVolt.units : _naString + } + + VehicleSummaryRow { + labelText: showBatteryIndex ? qsTr("Battery %1 Empty").arg(batteryIndex) : qsTr("Battery Empty") + valueText: battParams.battLowVoltAvailable ? battParams.battLowVolt.valueString + " " + battParams.battLowVolt.units : _naString + } - VehicleSummaryRow { - labelText: qsTr("Number of Cells") - valueText: battParams.battNumCellsAvailable ? battParams.battNumCells.valueString : _naString + VehicleSummaryRow { + labelText: showBatteryIndex ? qsTr("Battery %1 Number of Cells").arg(batteryIndex) : qsTr("Number of Cells") + valueText: battParams.battNumCellsAvailable ? battParams.battNumCells.valueString : _naString + } } } } diff --git a/src/Comms/MockLink/PX4MockLink.params b/src/Comms/MockLink/PX4MockLink.params index 7dc95d5c9df3..556fb48875c6 100644 --- a/src/Comms/MockLink/PX4MockLink.params +++ b/src/Comms/MockLink/PX4MockLink.params @@ -21,6 +21,15 @@ 1 1 BAT1_V_EMPTY 3.400000095367431641 9 1 1 BAT1_V_LOAD_DROP 0.300000011920928955 9 1 1 BAT1_V_OFFS_CURR 0.000000000000000000 9 +1 1 BAT2_A_PER_V 15.391030311584472656 9 +1 1 BAT2_CAPACITY -1.000000000000000000 9 +1 1 BAT2_N_CELLS 3 6 +1 1 BAT2_R_INTERNAL -1.000000000000000000 9 +1 1 BAT2_SOURCE 0 6 +1 1 BAT2_V_CHARGED 4.050000190734863281 9 +1 1 BAT2_V_DIV 10.177939414978027344 9 +1 1 BAT2_V_EMPTY 3.400000095367431641 9 +1 1 BAT2_V_LOAD_DROP 0.300000011920928955 9 1 1 CAL_ACC0_EN 1 6 1 1 CAL_ACC0_ID 1376264 6 1 1 CAL_ACC0_XOFF 0.001000000047497451 9 From 0d1b9b5fa1b2bc7a0d51e512935993f5224dae03 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 16:25:42 -0700 Subject: [PATCH 098/119] Remove windowTransparentText from palette windowTransparentText is a duplicate of text since windowTransparent is the same base color as window (just with alpha). Replace all usages of qgcPal.windowTransparentText with qgcPal.text and remove the palette entry. Fixes #13529 --- src/QmlControls/QGCPalette.cc | 1 - src/QmlControls/QGCPalette.h | 1 - src/QmlControls/ToolStripHoverButton.qml | 4 ++-- src/UI/toolbar/BatteryIndicator.qml | 4 ++-- src/UI/toolbar/EscIndicator.qml | 4 ++-- src/UI/toolbar/FlightModeIndicator.qml | 4 ++-- src/UI/toolbar/GPSIndicator.qml | 8 ++++---- src/UI/toolbar/GimbalIndicator.qml | 10 +++++----- src/UI/toolbar/MainStatusIndicator.qml | 6 +++--- 9 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/QmlControls/QGCPalette.cc b/src/QmlControls/QGCPalette.cc index af258e4001e3..5b5d339f2910 100644 --- a/src/QmlControls/QGCPalette.cc +++ b/src/QmlControls/QGCPalette.cc @@ -41,7 +41,6 @@ void QGCPalette::_buildMap() DECLARE_QGC_COLOR(windowShade, "#d9d9d9", "#d9d9d9", "#333333", "#333333") DECLARE_QGC_COLOR(windowShadeDark, "#bdbdbd", "#bdbdbd", "#282828", "#282828") DECLARE_QGC_COLOR(text, "#9d9d9d", "#333333", "#707070", "#ffffff") - DECLARE_QGC_COLOR(windowTransparentText,"#9d9d9d", "#000000", "#707070", "#ffffff") DECLARE_QGC_COLOR(warningText, "#cc0808", "#cc0808", "#f85761", "#f85761") DECLARE_QGC_COLOR(button, "#ffffff", "#ffffff", "#707070", "#626270") DECLARE_QGC_COLOR(buttonBorder, "#9d9d9d", "#3A9BDC", "#707070", "#adadb8") diff --git a/src/QmlControls/QGCPalette.h b/src/QmlControls/QGCPalette.h index fdd1dca00ca4..7c41c587b5ab 100644 --- a/src/QmlControls/QGCPalette.h +++ b/src/QmlControls/QGCPalette.h @@ -110,7 +110,6 @@ class QGCPalette : public QObject DEFINE_QGC_COLOR(windowShade, setWindowShade) DEFINE_QGC_COLOR(windowShadeDark, setWindowShadeDark) DEFINE_QGC_COLOR(text, setText) - DEFINE_QGC_COLOR(windowTransparentText, setWindowTransparentText) DEFINE_QGC_COLOR(warningText, setWarningText) DEFINE_QGC_COLOR(button, setButton) DEFINE_QGC_COLOR(buttonBorder, setButtonBorder) diff --git a/src/QmlControls/ToolStripHoverButton.qml b/src/QmlControls/ToolStripHoverButton.qml index b5ddae984414..52c58303b3fc 100644 --- a/src/QmlControls/ToolStripHoverButton.qml +++ b/src/QmlControls/ToolStripHoverButton.qml @@ -27,8 +27,8 @@ Button { property real imageScale: forceImageScale11 && (text == "") ? 0.8 : 0.6 property real contentMargins: innerText.height * 0.1 - property color _currentContentColor: (checked || pressed) ? qgcPal.buttonHighlightText : qgcPal.windowTransparentText - property color _currentContentColorSecondary: (checked || pressed) ? qgcPal.windowTransparentText : qgcPal.buttonHighlight + property color _currentContentColor: (checked || pressed) ? qgcPal.buttonHighlightText : qgcPal.text + property color _currentContentColorSecondary: (checked || pressed) ? qgcPal.text : qgcPal.buttonHighlight signal dropped(int index) diff --git a/src/UI/toolbar/BatteryIndicator.qml b/src/UI/toolbar/BatteryIndicator.qml index 5a97d42a9774..62ca564c673e 100644 --- a/src/UI/toolbar/BatteryIndicator.qml +++ b/src/UI/toolbar/BatteryIndicator.qml @@ -328,7 +328,7 @@ Item { QGCLabel { Layout.alignment: Qt.AlignHCenter verticalAlignment: Text.AlignVCenter - color: qgcPal.windowTransparentText + color: qgcPal.text text: getBatteryPercentageText() font.pointSize: _showBoth ? ScreenTools.defaultFontPointSize : ScreenTools.mediumFontPointSize visible: _showBoth || _showPercentage @@ -337,7 +337,7 @@ Item { QGCLabel { Layout.alignment: Qt.AlignHCenter font.pointSize: _showBoth ? ScreenTools.defaultFontPointSize : ScreenTools.mediumFontPointSize - color: qgcPal.windowTransparentText + color: qgcPal.text text: getBatteryVoltageText() visible: _showBoth || _showVoltage } diff --git a/src/UI/toolbar/EscIndicator.qml b/src/UI/toolbar/EscIndicator.qml index 7e18d0e52198..ff980ef5b145 100644 --- a/src/UI/toolbar/EscIndicator.qml +++ b/src/UI/toolbar/EscIndicator.qml @@ -73,7 +73,7 @@ Item { source: "/qmlimages/EscIndicator.svg" fillMode: Image.PreserveAspectFit sourceSize.height: height - color: qgcPal.windowTransparentText + color: qgcPal.text } Column { @@ -83,7 +83,7 @@ Item { QGCLabel { anchors.horizontalCenter: parent.horizontalCenter - color: qgcPal.windowTransparentText + color: qgcPal.text text: _onlineMotorCount.toString() font.pointSize: ScreenTools.smallFontPointSize } diff --git a/src/UI/toolbar/FlightModeIndicator.qml b/src/UI/toolbar/FlightModeIndicator.qml index b7b0cd93d0c2..7abfb380fb06 100644 --- a/src/UI/toolbar/FlightModeIndicator.qml +++ b/src/UI/toolbar/FlightModeIndicator.qml @@ -35,14 +35,14 @@ Item { Layout.preferredHeight: ScreenTools.defaultFontPixelHeight fillMode: Image.PreserveAspectFit mipmap: true - color: qgcPal.windowTransparentText + color: qgcPal.text source: "/qmlimages/FlightModesComponentIcon.png" } QGCLabel { id: flightModeLabel text: activeVehicle ? activeVehicle.flightMode : qsTr("N/A", "No data to display") - color: qgcPal.windowTransparentText + color: qgcPal.text font.pointSize: fontPointSize } diff --git a/src/UI/toolbar/GPSIndicator.qml b/src/UI/toolbar/GPSIndicator.qml index 6f65c67e0b2a..347f800b196e 100644 --- a/src/UI/toolbar/GPSIndicator.qml +++ b/src/UI/toolbar/GPSIndicator.qml @@ -32,7 +32,7 @@ Item { id: gpsLabel rotation: 90 text: qsTr("RTK") - color: qgcPal.windowTransparentText + color: qgcPal.text anchors.verticalCenter: parent.verticalCenter visible: _rtkConnected } @@ -46,7 +46,7 @@ Item { fillMode: Image.PreserveAspectFit sourceSize.height: height opacity: (_activeVehicle && _activeVehicle.gps.count.value >= 0) ? 1 : 0.5 - color: qgcPal.windowTransparentText + color: qgcPal.text } } @@ -58,13 +58,13 @@ Item { QGCLabel { anchors.horizontalCenter: hdopValue.horizontalCenter - color: qgcPal.windowTransparentText + color: qgcPal.text text: _activeVehicle ? _activeVehicle.gps.count.valueString : "" } QGCLabel { id: hdopValue - color: qgcPal.windowTransparentText + color: qgcPal.text text: _activeVehicle ? _activeVehicle.gps.hdop.value.toFixed(1) : "" } } diff --git a/src/UI/toolbar/GimbalIndicator.qml b/src/UI/toolbar/GimbalIndicator.qml index 95763056c564..4f71d4ff58dd 100644 --- a/src/UI/toolbar/GimbalIndicator.qml +++ b/src/UI/toolbar/GimbalIndicator.qml @@ -52,7 +52,7 @@ Item { source: "/res/CameraGimbal.png" fillMode: Image.PreserveAspectFit sourceSize.height: height - color: qgcPal.windowTransparentText + color: qgcPal.text } @@ -61,7 +61,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter font.pointSize: ScreenTools.smallFontPointSize text: activeGimbal ? activeGimbal.deviceId.rawValue : "" - color: qgcPal.windowTransparentText + color: qgcPal.text visible: multiGimbalSetup } } @@ -80,7 +80,7 @@ Item { text: activeGimbal && activeGimbal.retracted ? qsTr("Retracted") : (activeGimbal && activeGimbal.yawLock ? qsTr("Yaw locked") : qsTr("Yaw follow")) - color: qgcPal.windowTransparentText + color: qgcPal.text Layout.columnSpan: 2 Layout.alignment: Qt.AlignHCenter } @@ -88,7 +88,7 @@ Item { id: pitchLabel font.pointSize: ScreenTools.smallFontPointSize text: activeGimbal ? qsTr("P: ") + activeGimbal.absolutePitch.valueString : "" - color: qgcPal.windowTransparentText + color: qgcPal.text } QGCLabel { id: panLabel @@ -98,7 +98,7 @@ Item { (qsTr("Az: ") + activeGimbal.absoluteYaw.valueString) : (qsTr("Y: ") + activeGimbal.bodyYaw.valueString)) : "" - color: qgcPal.windowTransparentText + color: qgcPal.text } } } diff --git a/src/UI/toolbar/MainStatusIndicator.qml b/src/UI/toolbar/MainStatusIndicator.qml index 92f57fea9d9a..003d924073ba 100644 --- a/src/UI/toolbar/MainStatusIndicator.qml +++ b/src/UI/toolbar/MainStatusIndicator.qml @@ -31,7 +31,7 @@ RowLayout { Layout.preferredWidth: contentWidth + (vehicleMessagesIcon.visible ? vehicleMessagesIcon.width + control.spacing : 0) verticalAlignment: Text.AlignVCenter text: mainStatusText() - color: qgcPal.windowTransparentText + color: qgcPal.text font.pointSize: ScreenTools.largeFontPointSize property string _commLostText: qsTr("Comms Lost") @@ -120,7 +120,7 @@ RowLayout { visible: _activeVehicle && _activeVehicle.messageCount > 0 function getIconColor() { - let iconColor = qgcPal.windowTransparentText + let iconColor = qgcPal.text if (_activeVehicle) { if (_activeVehicle.messageTypeWarning) { iconColor = qgcPal.colorOrange @@ -143,7 +143,7 @@ RowLayout { Layout.fillHeight: true verticalAlignment: Text.AlignVCenter text: _vtolInFWDFlight ? qsTr("FW(vtol)") : qsTr("MR(vtol)") - color: qgcPal.windowTransparentText + color: qgcPal.text font.pointSize: _vehicleInAir ? ScreenTools.largeFontPointSize : ScreenTools.defaultFontPointSize visible: _activeVehicle && _activeVehicle.vtol From 2ca4fc7e7c75b770e02541a0fe3a3a6f4b3e99bd Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 17:07:15 -0700 Subject: [PATCH 099/119] Add unit test for CalcAboveTerrain save/load roundtrip Regression test for https://github.com/mavlink/qgroundcontrol/issues/13513 Verifies that saving and loading a SimpleMissionItem with AltitudeFrameCalcAboveTerrain preserves the altitude frame, above-terrain altitude, AMSL altitude, and param7 correctly. --- test/MissionManager/SimpleMissionItemTest.cc | 44 ++++++++++++++++++++ test/MissionManager/SimpleMissionItemTest.h | 1 + 2 files changed, 45 insertions(+) diff --git a/test/MissionManager/SimpleMissionItemTest.cc b/test/MissionManager/SimpleMissionItemTest.cc index b666ba5c9d14..b75002e895d1 100644 --- a/test/MissionManager/SimpleMissionItemTest.cc +++ b/test/MissionManager/SimpleMissionItemTest.cc @@ -328,4 +328,48 @@ void SimpleMissionItemTest::_testAltitudePropogation() QCOMPARE(_simpleItem->missionItem().frame(), MAV_FRAME_GLOBAL); } +void SimpleMissionItemTest::_testCalcAboveTerrainSaveLoad() +{ + // Regression test for https://github.com/mavlink/qgroundcontrol/issues/13513 + // When saving/loading a mission with CalcAboveTerrain altitude mode, the AMSL altitude + // must be preserved correctly rather than being overwritten with the above-terrain value. + + const double terrainAlt = 500.0; // Terrain elevation at the waypoint + const double aboveTerrainAlt = 250.0; // User-specified height above terrain + const double amslAlt = terrainAlt + aboveTerrainAlt; + + // Setup the item with CalcAboveTerrain mode and simulate terrain calculation having completed + _simpleItem->setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain); + _simpleItem->altitude()->setRawValue(aboveTerrainAlt); + _simpleItem->amslAltAboveTerrain()->setRawValue(amslAlt); + // For CalcAboveTerrain, param7 should be the AMSL altitude + _simpleItem->missionItem().setParam7(amslAlt); + + // Save + QJsonArray missionItems; + _simpleItem->save(missionItems); + QVERIFY(missionItems.count() > 0); + QJsonObject savedJson = missionItems[0].toObject(); + + // Verify saved JSON contains the correct altitude data + QVERIFY(savedJson.contains("AltitudeMode")); + QVERIFY(savedJson.contains("Altitude")); + QVERIFY(savedJson.contains("AMSLAltAboveTerrain")); + QCOMPARE(savedJson["AltitudeMode"].toInt(), static_cast(QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain)); + QCOMPARE(savedJson["Altitude"].toDouble(), aboveTerrainAlt); + QCOMPARE(savedJson["AMSLAltAboveTerrain"].toDouble(), amslAlt); + + // Load into a new SimpleMissionItem (forLoad=true matches production MissionController behavior) + QString errorString; + SimpleMissionItem loadedItem(planController(), false /* flyView */, true /* forLoad */); + QVERIFY(loadedItem.load(savedJson, 1, errorString)); + + // Verify state immediately after load — before any terrain query + QCOMPARE(loadedItem.altitudeFrame(), QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain); + QCOMPARE(loadedItem.altitude()->rawValue().toDouble(), aboveTerrainAlt); + QCOMPARE(loadedItem.amslAltAboveTerrain()->rawValue().toDouble(), amslAlt); + QCOMPARE(loadedItem.missionItem().frame(), MAV_FRAME_GLOBAL); + QCOMPARE(loadedItem.missionItem().param7(), amslAlt); +} + UT_REGISTER_TEST(SimpleMissionItemTest, TestLabel::Unit, TestLabel::MissionManager) diff --git a/test/MissionManager/SimpleMissionItemTest.h b/test/MissionManager/SimpleMissionItemTest.h index f687e0fa6224..59d438c344af 100644 --- a/test/MissionManager/SimpleMissionItemTest.h +++ b/test/MissionManager/SimpleMissionItemTest.h @@ -28,6 +28,7 @@ private slots: void _testCameraSection(); void _testSpeedSection(); void _testAltitudePropogation(); + void _testCalcAboveTerrainSaveLoad(); private: void _testEditorFactsWorker(QGCMAVLink::VehicleClass_t vehicleClass, QGCMAVLink::VehicleClass_t vtolMode); From 19d3c0429f483380943573e321f12556754b9cee Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 15:28:40 -0700 Subject: [PATCH 100/119] PlanView QML: Use setSource() and required properties to fix null warnings PlanTreeView: Replace sourceComponent bindings with setSource() calls in Component.onCompleted. This injects required properties (missionItem, map, etc.) between beginCreate() and completeCreate(), preventing 'Cannot read property of null' warnings that occurred when sourceComponent bindings raced with internal property initialization. MissionItemEditor: Add required properties for missionItem and map. Use setSource() for the editor Loader. Derive masterController from missionItem.masterController instead of receiving it as a context property. Sub-editors (FWLandingPatternEditor, VTOLLandingPatternEditor, StructureScanEditor, MissionSettingsEditor, SimpleItemEditor, TransectStyleComplexItemEditor): Add required property declarations for missionItem and availableWidth. CameraSection: Add required missionItem property so it no longer relies on implicit context property resolution up the hierarchy chain. --- src/PlanView/CameraSection.qml | 2 + src/PlanView/CorridorScanEditor.qml | 4 - src/PlanView/FWLandingPatternEditor.qml | 7 +- src/PlanView/MissionItemEditor.qml | 42 ++-- src/PlanView/MissionSettingsEditor.qml | 6 +- src/PlanView/PlanTreeView.qml | 210 ++++++++---------- src/PlanView/SimpleItemEditor.qml | 5 + src/PlanView/StructureScanEditor.qml | 5 +- src/PlanView/SurveyItemEditor.qml | 4 - .../TransectStyleComplexItemEditor.qml | 3 + src/PlanView/VTOLLandingPatternEditor.qml | 7 +- .../MissionCommandTreeEditorTestWindow.qml | 13 +- 12 files changed, 151 insertions(+), 157 deletions(-) diff --git a/src/PlanView/CameraSection.qml b/src/PlanView/CameraSection.qml index 34b7dee95dd1..f970221d2865 100644 --- a/src/PlanView/CameraSection.qml +++ b/src/PlanView/CameraSection.qml @@ -8,6 +8,8 @@ import QGroundControl.FactControls // Camera section for mission item editors Column { + required property var missionItem + property alias buttonGroup: cameraSectionHeader.buttonGroup property alias showSpacer: cameraSectionHeader.showSpacer property alias checked: cameraSectionHeader.checked diff --git a/src/PlanView/CorridorScanEditor.qml b/src/PlanView/CorridorScanEditor.qml index c0c9e6fe1651..8d7a1d2e7f9a 100644 --- a/src/PlanView/CorridorScanEditor.qml +++ b/src/PlanView/CorridorScanEditor.qml @@ -15,10 +15,6 @@ TransectStyleComplexItemEditor { transectValuesComponent: _transectValuesComponent presetsTransectValuesComponent: _transectValuesComponent - // The following properties must be available up the hierarchy chain - // property real availableWidth ///< Width for control - // property var missionItem ///< Mission Item for editor - property real _margin: ScreenTools.defaultFontPixelWidth / 2 property var _missionItem: missionItem diff --git a/src/PlanView/FWLandingPatternEditor.qml b/src/PlanView/FWLandingPatternEditor.qml index 5c2b7bcfdde3..e4420b4c332e 100644 --- a/src/PlanView/FWLandingPatternEditor.qml +++ b/src/PlanView/FWLandingPatternEditor.qml @@ -15,11 +15,10 @@ Rectangle { color: qgcPal.windowShadeDark radius: _radius - // The following properties must be available up the hierarchy chain - //property real availableWidth ///< Width for control - //property var missionItem ///< Mission Item for editor + required property var missionItem + required property real availableWidth - property var _masterControler: masterController + property var _masterControler: missionItem.masterController property var _missionController: _masterControler.missionController property var _missionVehicle: _masterControler.controllerVehicle property real _margin: ScreenTools.defaultFontPixelWidth / 2 diff --git a/src/PlanView/MissionItemEditor.qml b/src/PlanView/MissionItemEditor.qml index f50079acaed0..450821814c8b 100644 --- a/src/PlanView/MissionItemEditor.qml +++ b/src/PlanView/MissionItemEditor.qml @@ -10,6 +10,13 @@ import QGroundControl.FactControls /// Mission item edit control Rectangle { + required property var missionItem ///< MissionItem associated with this editor + required property var map ///< Map control + + signal clicked + signal remove + signal selectNextNotReadyItem + id: _root height: _currentItem ? (editorLoader.y + editorLoader.height + _innerMargin) : (topRowLayout.y + topRowLayout.height + _margin) color: _currentItem ? qgcPal.buttonHighlight : qgcPal.windowShade @@ -18,16 +25,7 @@ Rectangle { border.width: _readyForSave ? 0 : 2 border.color: qgcPal.warningText - property var map ///< Map control - property var masterController - property var missionItem ///< MissionItem associated with this editor - property bool readOnly ///< true: read only view, false: full editing view - - signal clicked - signal remove - signal selectNextNotReadyItem - - property var _masterController: masterController + property var _masterController: missionItem.masterController property var _missionController: _masterController.missionController property bool _currentItem: missionItem.isCurrentItem property color _outerTextColor: _currentItem ? qgcPal.buttonHighlightText : qgcPal.text @@ -44,6 +42,23 @@ Rectangle { readonly property real _trashSize: commandPicker.height * 0.75 readonly property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly + // setSource() injects missionItem before internal bindings activate + function _loadEditor() { + if (missionItem.isCurrentItem) { + editorLoader.setSource(missionItem.editorQml, { + missionItem: _root.missionItem, + availableWidth: _root.width - (editorLoader.anchors.margins * 2) + }) + } else { + editorLoader.setSource("") + } + } + + Connections { + target: missionItem + function onIsCurrentItemChanged() { _root._loadEditor() } + } + QGCPalette { id: qgcPal colorGroupEnabled: enabled @@ -168,7 +183,7 @@ Rectangle { id: commandDialog MissionCommandDialog { - vehicle: masterController.controllerVehicle + vehicle: _masterController.controllerVehicle missionItem: _root.missionItem map: _root.map // FIXME: Disabling fly through commands doesn't work since you may need to change from an RTL to something else @@ -321,11 +336,8 @@ Rectangle { anchors.margins: _innerMargin anchors.left: parent.left anchors.top: topRowLayout.bottom - source: _currentItem ? missionItem.editorQml : "" asynchronous: true - property var masterController: _masterController - property real availableWidth: _root.width - (anchors.margins * 2) ///< How wide the editor should be - property var editorRoot: _root + Component.onCompleted: _root._loadEditor() } } diff --git a/src/PlanView/MissionSettingsEditor.qml b/src/PlanView/MissionSettingsEditor.qml index 962be789cdf6..9e17406a38b1 100644 --- a/src/PlanView/MissionSettingsEditor.qml +++ b/src/PlanView/MissionSettingsEditor.qml @@ -14,7 +14,10 @@ Rectangle { color: qgcPal.windowShadeDark radius: ScreenTools.defaultBorderRadius - property var _masterController: masterController + required property var missionItem + required property real availableWidth + + property var _masterController: missionItem.masterController property var _controllerVehicle: _masterController.controllerVehicle property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly property bool _showCameraSection: _waypointsOnlyMode || QGroundControl.corePlugin.showAdvancedUI @@ -41,6 +44,7 @@ Rectangle { id: cameraSection anchors.left: parent.left anchors.right: parent.right + missionItem: root.missionItem visible: _showCameraSection showSectionHeader: false diff --git a/src/PlanView/PlanTreeView.qml b/src/PlanView/PlanTreeView.qml index 5438b936200e..b1d61c38d56d 100644 --- a/src/PlanView/PlanTreeView.qml +++ b/src/PlanView/PlanTreeView.qml @@ -126,32 +126,106 @@ TreeView { readonly property string nodeType: model.nodeType readonly property bool separator: model.separator ?? false - onImplicitHeightChanged: layoutTimer.restart() + readonly property string _qrcBase: "qrc:/qml/QGroundControl/PlanView/" + + // We use setSource() instead of sourceComponent so that required properties + // (e.g. missionItem) are injected before internal bindings activate, + // preventing "Cannot read property of null" warnings. Loader { id: loader width: parent.width - sourceComponent: { - // Guard: non-group delegates need a valid object. During model - // row removal the role data goes null before the delegate is - // destroyed, which would cause "Cannot read property of null" - // warnings in every downstream editor binding. + + Component.onCompleted: { switch (delegateRoot.nodeType) { - case "planFileGroup": return groupHeaderComponent - case "defaultsGroup": return groupHeaderComponent - case "missionGroup": return groupHeaderComponent - case "fenceGroup": return groupHeaderComponent - case "rallyGroup": return groupHeaderComponent - case "transformGroup": return groupHeaderComponent - case "planFileInfo": return planFileInfoComponent - case "defaultsInfo": return defaultsEditorComponent - case "missionItem": return delegateRoot.nodeObject ? missionItemComponent : null - case "fenceEditor": return delegateRoot.nodeObject ? fenceEditorComponent : null - case "rallyHeader": return delegateRoot.nodeObject ? rallyHeaderComponent : null - case "rallyItem": return delegateRoot.nodeObject ? rallyItemComponent : null - case "transformEditor": return transformEditorComponent - default: return null + case "planFileGroup": + case "defaultsGroup": + case "missionGroup": + case "fenceGroup": + case "rallyGroup": + case "transformGroup": + sourceComponent = groupHeaderComponent + break + case "planFileInfo": + setSource(delegateRoot._qrcBase + "PlanInfoEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + planMasterController: root.planMasterController, + missionController: root._missionController, + editorMap: root.editorMap + }) + break + case "defaultsInfo": + setSource(delegateRoot._qrcBase + "MissionDefaultsEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + missionController: root._missionController, + planMasterController: root.planMasterController + }) + break + case "missionItem": + if (delegateRoot.nodeObject) { + setSource(delegateRoot._qrcBase + "MissionItemEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + map: root.editorMap, + missionItem: delegateRoot.nodeObject + }) + } + break + case "fenceEditor": + if (delegateRoot.nodeObject) { + setSource(delegateRoot._qrcBase + "GeoFenceEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + myGeoFenceController: root._geoFenceController, + flightMap: root.editorMap + }) + } + break + case "rallyHeader": + if (delegateRoot.nodeObject) { + setSource(delegateRoot._qrcBase + "RallyPointEditorHeader.qml", { + width: Qt.binding(() => delegateRoot.width), + controller: root._rallyPointController + }) + } + break + case "rallyItem": + if (delegateRoot.nodeObject) { + setSource(delegateRoot._qrcBase + "RallyPointItemEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + rallyPoint: delegateRoot.nodeObject, + controller: root._rallyPointController + }) + } + break + case "transformEditor": + setSource(delegateRoot._qrcBase + "TransformEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + missionController: root._missionController + }) + break + } + } + + onLoaded: { + if (delegateRoot.nodeType === "missionItem" && item) { + item.clicked.connect(function() { + root._missionController.setCurrentPlanViewSeqNum(delegateRoot.nodeObject.sequenceNumber, false) + }) + item.remove.connect(function() { + var viIndex = root._missionController.visualItemIndexForObject(delegateRoot.nodeObject) + if (viIndex > 0) { + root._missionController.removeVisualItem(viIndex) + } + }) + item.selectNextNotReadyItem.connect(function() { + for (var i = 0; i < root._missionController.visualItems.count; i++) { + var vmi = root._missionController.visualItems.get(i) + if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) { + root._missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true) + break + } + } + }) } } } @@ -217,101 +291,5 @@ TreeView { } } - // ── Plan info delegate ── - Component { - id: planFileInfoComponent - - PlanInfoEditor { - width: delegateRoot.width - planMasterController: root.planMasterController - missionController: root._missionController - editorMap: root.editorMap - } - } - - // ── Defaults editor delegate ── - Component { - id: defaultsEditorComponent - - MissionDefaultsEditor { - width: delegateRoot.width - missionController: root._missionController - planMasterController: root.planMasterController - } - } - - // ── Mission item delegate ── - Component { - id: missionItemComponent - - MissionItemEditor { - width: delegateRoot.width - map: root.editorMap - masterController: root.planMasterController - missionItem: delegateRoot.nodeObject - readOnly: false - - onClicked: root._missionController.setCurrentPlanViewSeqNum(delegateRoot.nodeObject.sequenceNumber, false) - - onRemove: { - var viIndex = root._missionController.visualItemIndexForObject(delegateRoot.nodeObject) - if (viIndex > 0) { - root._missionController.removeVisualItem(viIndex) - } - } - - onSelectNextNotReadyItem: { - for (var i = 0; i < root._missionController.visualItems.count; i++) { - var vmi = root._missionController.visualItems.get(i) - if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) { - root._missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true) - break - } - } - } - } - } - - // ── GeoFence editor (single child of fence group) ── - Component { - id: fenceEditorComponent - - GeoFenceEditor { - width: delegateRoot.width - myGeoFenceController: root._geoFenceController - flightMap: root.editorMap - } - } - - // ── Rally header / instructions ── - Component { - id: rallyHeaderComponent - - RallyPointEditorHeader { - width: delegateRoot.width - controller: root._rallyPointController - } - } - - // ── Rally point item editor ── - Component { - id: rallyItemComponent - - RallyPointItemEditor { - width: delegateRoot.width - rallyPoint: delegateRoot.nodeObject - controller: root._rallyPointController - } - } - - // ── Transform editor (single child of transform group) ── - Component { - id: transformEditorComponent - - TransformEditor { - width: delegateRoot.width - missionController: root._missionController - } - } } } diff --git a/src/PlanView/SimpleItemEditor.qml b/src/PlanView/SimpleItemEditor.qml index c984d1b7223b..dc476dc4aa5a 100644 --- a/src/PlanView/SimpleItemEditor.qml +++ b/src/PlanView/SimpleItemEditor.qml @@ -8,12 +8,16 @@ import QGroundControl.FactControls // Editor for Simple mission items Rectangle { + required property var missionItem + required property real availableWidth + id: root width: availableWidth height: editorColumn.height + (_margin * 2) color: qgcPal.windowShadeDark radius: _radius + property bool _specifiesAltitude: missionItem.specifiesAltitude property real _margin: ScreenTools.defaultFontPixelHeight / 2 property real _altRectMargin: ScreenTools.defaultFontPixelWidth / 2 @@ -243,6 +247,7 @@ Rectangle { CameraSection { Layout.fillWidth: true showSectionHeader: false + missionItem: root.missionItem visible: tabBar.currentIndex === 1 Component.onCompleted: checked = missionItem.cameraSection.settingsSpecified diff --git a/src/PlanView/StructureScanEditor.qml b/src/PlanView/StructureScanEditor.qml index 4c183233400d..56c090f7cddc 100644 --- a/src/PlanView/StructureScanEditor.qml +++ b/src/PlanView/StructureScanEditor.qml @@ -16,9 +16,8 @@ Rectangle { color: qgcPal.windowShadeDark radius: _radius - // The following properties must be available up the hierarchy chain - //property real availableWidth ///< Width for control - //property var missionItem ///< Mission Item for editor + required property var missionItem + required property real availableWidth property real _margin: ScreenTools.defaultFontPixelWidth / 2 property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 10.5 diff --git a/src/PlanView/SurveyItemEditor.qml b/src/PlanView/SurveyItemEditor.qml index 7db2dfbf6d34..3bc82a05b3ad 100644 --- a/src/PlanView/SurveyItemEditor.qml +++ b/src/PlanView/SurveyItemEditor.qml @@ -15,10 +15,6 @@ TransectStyleComplexItemEditor { transectValuesComponent: _transectValuesComponent presetsTransectValuesComponent: _transectValuesComponent - // The following properties must be available up the hierarchy chain - // property real availableWidth ///< Width for control - // property var missionItem ///< Mission Item for editor - property real _margin: ScreenTools.defaultFontPixelWidth / 2 property var _missionItem: missionItem diff --git a/src/PlanView/TransectStyleComplexItemEditor.qml b/src/PlanView/TransectStyleComplexItemEditor.qml index 68b6e2a953ef..a9d478d16984 100644 --- a/src/PlanView/TransectStyleComplexItemEditor.qml +++ b/src/PlanView/TransectStyleComplexItemEditor.qml @@ -15,6 +15,9 @@ Rectangle { color: qgcPal.windowShadeDark radius: _radius + required property var missionItem + required property real availableWidth + property bool transectAreaDefinitionComplete: true property string transectAreaDefinitionHelp: _internalError property string transectValuesHeaderName: _internalError diff --git a/src/PlanView/VTOLLandingPatternEditor.qml b/src/PlanView/VTOLLandingPatternEditor.qml index 9f349efba09f..77cc58a678e6 100644 --- a/src/PlanView/VTOLLandingPatternEditor.qml +++ b/src/PlanView/VTOLLandingPatternEditor.qml @@ -15,11 +15,10 @@ Rectangle { color: qgcPal.windowShadeDark radius: _radius - // The following properties must be available up the hierarchy chain - //property real availableWidth ///< Width for control - //property var missionItem ///< Mission Item for editor + required property var missionItem + required property real availableWidth - property var _masterControler: masterController + property var _masterControler: missionItem.masterController property var _missionController: _masterControler.missionController property var _missionVehicle: _masterControler.controllerVehicle property real _margin: ScreenTools.defaultFontPixelWidth / 2 diff --git a/test/MissionManager/MissionCommandTreeEditorTestWindow.qml b/test/MissionManager/MissionCommandTreeEditorTestWindow.qml index 4b89eb13145e..c1f43b27bc9a 100644 --- a/test/MissionManager/MissionCommandTreeEditorTestWindow.qml +++ b/test/MissionManager/MissionCommandTreeEditorTestWindow.qml @@ -41,12 +41,13 @@ ApplicationWindow { QGCLabel { text: modelData.commandName; color: "black" } Loader { - id: editorLoader - source: modelData.editorQml - - property var missionItem: modelData - property var masterController: planMasterController - property real availableWidth: editorWidth + id: editorLoader + Component.onCompleted: { + editorLoader.setSource(modelData.editorQml, { + missionItem: modelData, + availableWidth: editorWidth + }) + } } } } From 7224f82c5e388349dbcea94fadc5663b2f5c3f6f Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 15:28:51 -0700 Subject: [PATCH 101/119] Fix model correctness in QmlObjectTreeModel and QmlObjectListModel QmlObjectTreeModel::removeChildren: Defer TreeNode deletion until after endRemoveRows() completes. Views may still access node data during the removal notification window; freeing nodes prematurely caused stale pointer access. QmlObjectTreeModel::removeItem: Skip beginRemoveRows/endRemoveRows when inside a batch reset (_resetModelNestingCount > 0). Also skip separator signal emission during resets to avoid redundant notifications. QmlObjectListModel::insertRows/removeRows: Add _resetModelNestingCount checks to skip begin/end row signals when already inside a batch reset, matching the existing pattern in QmlObjectTreeModel. --- src/QmlControls/QmlObjectListModel.cc | 14 ++++++++++---- src/QmlControls/QmlObjectTreeModel.cc | 23 +++++++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/QmlControls/QmlObjectListModel.cc b/src/QmlControls/QmlObjectListModel.cc index 7532e2057741..eb88d3ffac12 100644 --- a/src/QmlControls/QmlObjectListModel.cc +++ b/src/QmlControls/QmlObjectListModel.cc @@ -110,8 +110,10 @@ bool QmlObjectListModel::insertRows(int position, int rows, const QModelIndex& p qCWarning(QmlObjectListModelLog) << "Invalid position - position:count" << position << _objectList.count() << this; } - beginInsertRows(QModelIndex(), position, position + rows - 1); - endInsertRows(); + if (_resetModelNestingCount == 0) { + beginInsertRows(QModelIndex(), position, position + rows - 1); + endInsertRows(); + } _signalCountChangedIfNotNested(); @@ -128,11 +130,15 @@ bool QmlObjectListModel::removeRows(int position, int rows, const QModelIndex& p qCWarning(QmlObjectListModelLog) << "Invalid rows - position:rows:count" << position << rows << _objectList.count() << this; } - beginRemoveRows(QModelIndex(), position, position + rows - 1); + if (_resetModelNestingCount == 0) { + beginRemoveRows(QModelIndex(), position, position + rows - 1); + } for (int row=0; rowchildren.removeAt(row); - endRemoveRows(); + if (_resetModelNestingCount == 0) { + endRemoveRows(); + } // Free the subtree's TreeNode objects but NOT the QObjects _deleteSubtree(node, false); delete node; _totalCount -= removedCount; - if (!parentNode->children.isEmpty()) { + if (_resetModelNestingCount == 0 && !parentNode->children.isEmpty()) { _emitSeparatorChanged(parentIdx, parentNode->children.count() - 1); } _signalCountChangedIfNotNested(); @@ -399,19 +403,26 @@ void QmlObjectTreeModel::removeChildren(const QModelIndex& parentIndex) beginRemoveRows(parentIndex, 0, childCount - 1); } + // Disconnect signals but don't free nodes yet — views may still access them for (TreeNode* child : parentNode->children) { _disconnectSubtree(child); - _deleteSubtree(child, false); - delete child; } - parentNode->children.clear(); + // Detach from parent + QList orphans = parentNode->children; + parentNode->children.clear(); _totalCount -= removedCount; if (_resetModelNestingCount == 0) { endRemoveRows(); _signalCountChangedIfNotNested(); } + + // Now safe to free the TreeNode structs + for (TreeNode* child : orphans) { + _deleteSubtree(child, false); + delete child; + } } void QmlObjectTreeModel::clear() From 47de17ddee2a7f2c5185be8e48838e37e0ac2bf7 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 15:28:59 -0700 Subject: [PATCH 102/119] MissionController: Improve tree sync with row-level signals and renames Replace model-level beginResetModel/endResetModel in _syncTreeMissionItemsReset and _syncTreeRallyPointsReset with row-level signals from removeChildren() and appendItem(). This avoids unnecessarily invalidating all QPersistentModelIndex instances (the 6 group indexes) when only children of a single group are being manipulated. Wrap _setupTreeModel() in beginResetModel/endResetModel and capture persistent group indexes after endResetModel so they are not immediately invalidated. Split _syncTreeRallyPointsAboutToBeRemoved into two handlers: - aboutToBeRemoved: Only removes tree children (safe, indexes still valid) - rowsRemoved: Re-adds the header marker after the source model finishes removing (final state available, safe to insert) Rename all tree sync handlers from _on* prefix to _syncTree* prefix to clearly indicate their purpose of propagating source model changes into the _visualItemsTree secondary model. Use targeted disconnect(visualItem, nullptr, this, nullptr) in _deinitVisualItem instead of a full wildcard disconnect, preserving internal Qt destroyed-signal connections that models rely on. --- src/MissionManager/MissionController.cc | 112 +++++++++++++++--------- src/MissionManager/MissionController.h | 13 +-- 2 files changed, 76 insertions(+), 49 deletions(-) diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 1f372bf3b21c..f4395fda2470 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -1818,64 +1818,73 @@ void MissionController::_recalcChildItems(void) void MissionController::_setupTreeModel(void) { - // Tree starts empty — no need for beginReset/endReset. - // We use incremental inserts so QPersistentModelIndex stays valid. + _visualItemsTree.beginResetModel(); + _visualItemsTree.clear(); // ── Plan File group ── _planFileGroupNode.setObjectName(tr("Plan Info")); - _planFileGroupIndex = QPersistentModelIndex( - _visualItemsTree.appendItem(&_planFileGroupNode, QModelIndex(), QStringLiteral("planFileGroup"))); + _visualItemsTree.appendItem(&_planFileGroupNode, QModelIndex(), QStringLiteral("planFileGroup")); _planFileInfoMarker.setObjectName(QStringLiteral("planFileInfo")); - _visualItemsTree.appendItem(&_planFileInfoMarker, _planFileGroupIndex, QStringLiteral("planFileInfo")); + _visualItemsTree.appendItem(&_planFileInfoMarker, + _visualItemsTree.indexForObject(&_planFileGroupNode), QStringLiteral("planFileInfo")); // ── Defaults group ── _defaultsGroupNode.setObjectName(tr("Defaults")); - _defaultsGroupIndex = QPersistentModelIndex( - _visualItemsTree.appendItem(&_defaultsGroupNode, QModelIndex(), QStringLiteral("defaultsGroup"))); + _visualItemsTree.appendItem(&_defaultsGroupNode, QModelIndex(), QStringLiteral("defaultsGroup")); _defaultsInfoMarker.setObjectName(QStringLiteral("defaultsInfo")); - _visualItemsTree.appendItem(&_defaultsInfoMarker, _defaultsGroupIndex, QStringLiteral("defaultsInfo")); + _visualItemsTree.appendItem(&_defaultsInfoMarker, + _visualItemsTree.indexForObject(&_defaultsGroupNode), QStringLiteral("defaultsInfo")); // ── Mission Items group ── _missionItemsGroupNode.setObjectName(tr("Mission Items")); - _missionGroupIndex = QPersistentModelIndex( - _visualItemsTree.appendItem(&_missionItemsGroupNode, QModelIndex(), QStringLiteral("missionGroup"))); + _visualItemsTree.appendItem(&_missionItemsGroupNode, QModelIndex(), QStringLiteral("missionGroup")); // ── GeoFence group ── _fenceGroupNode.setObjectName(tr("GeoFence")); - _fenceGroupIndex = QPersistentModelIndex( - _visualItemsTree.appendItem(&_fenceGroupNode, QModelIndex(), QStringLiteral("fenceGroup"))); + _visualItemsTree.appendItem(&_fenceGroupNode, QModelIndex(), QStringLiteral("fenceGroup")); // Single marker child — delegate loads the full GeoFenceEditor _fenceEditorMarker.setObjectName(QStringLiteral("fenceEditor")); - _visualItemsTree.appendItem(&_fenceEditorMarker, _fenceGroupIndex, QStringLiteral("fenceEditor")); + _visualItemsTree.appendItem(&_fenceEditorMarker, + _visualItemsTree.indexForObject(&_fenceGroupNode), QStringLiteral("fenceEditor")); // ── Rally Points group ── _rallyGroupNode.setObjectName(tr("Rally Points")); - _rallyGroupIndex = QPersistentModelIndex( - _visualItemsTree.appendItem(&_rallyGroupNode, QModelIndex(), QStringLiteral("rallyGroup"))); + _visualItemsTree.appendItem(&_rallyGroupNode, QModelIndex(), QStringLiteral("rallyGroup")); // Marker child for the rally header / instructions _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader")); - _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader")); + _visualItemsTree.appendItem(&_rallyHeaderMarker, + _visualItemsTree.indexForObject(&_rallyGroupNode), QStringLiteral("rallyHeader")); // ── Transform group ── _transformGroupNode.setObjectName(tr("Transform")); - _transformGroupIndex = QPersistentModelIndex( - _visualItemsTree.appendItem(&_transformGroupNode, QModelIndex(), QStringLiteral("transformGroup"))); + _visualItemsTree.appendItem(&_transformGroupNode, QModelIndex(), QStringLiteral("transformGroup")); // Single marker child — delegate loads the inline TransformEditor _transformEditorMarker.setObjectName(QStringLiteral("transformEditor")); - _visualItemsTree.appendItem(&_transformEditorMarker, _transformGroupIndex, QStringLiteral("transformEditor")); + _visualItemsTree.appendItem(&_transformEditorMarker, + _visualItemsTree.indexForObject(&_transformGroupNode), QStringLiteral("transformEditor")); + + _visualItemsTree.endResetModel(); + + // Capture persistent indexes after the reset so they aren't invalidated + _planFileGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_planFileGroupNode)); + _defaultsGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_defaultsGroupNode)); + _missionGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_missionItemsGroupNode)); + _fenceGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_fenceGroupNode)); + _rallyGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_rallyGroupNode)); + _transformGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_transformGroupNode)); } //----------------------------------------------------------------------------- // Incremental tree model sync — Mission Items //----------------------------------------------------------------------------- -void MissionController::_onMissionItemsInserted(const QModelIndex& parent, int first, int last) +void MissionController::_syncTreeMissionItemsInserted(const QModelIndex& parent, int first, int last) { Q_UNUSED(parent); for (int i = first; i <= last; i++) { @@ -1886,7 +1895,7 @@ void MissionController::_onMissionItemsInserted(const QModelIndex& parent, int f } } -void MissionController::_onMissionItemsAboutToBeRemoved(const QModelIndex& parent, int first, int last) +void MissionController::_syncTreeMissionItemsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { Q_UNUSED(parent); // Remove in reverse order so indexes stay valid @@ -1895,9 +1904,10 @@ void MissionController::_onMissionItemsAboutToBeRemoved(const QModelIndex& paren } } -void MissionController::_onMissionItemsReset(void) +void MissionController::_syncTreeMissionItemsReset(void) { - // Clear mission group children and repopulate from _visualItems + // removeChildren + appendItem each fire their own row-level signals scoped + // to _missionGroupIndex, so persistent indexes for other groups survive. _visualItemsTree.removeChildren(_missionGroupIndex); if (_visualItems) { @@ -1914,7 +1924,7 @@ void MissionController::_onMissionItemsReset(void) // Incremental tree model sync — Rally Points //----------------------------------------------------------------------------- -void MissionController::_onRallyPointsInserted(const QModelIndex& parent, int first, int last) +void MissionController::_syncTreeRallyPointsInserted(const QModelIndex& parent, int first, int last) { Q_UNUSED(parent); auto* rallyController = _masterController->rallyPointController(); @@ -1934,24 +1944,32 @@ void MissionController::_onRallyPointsInserted(const QModelIndex& parent, int fi } } -void MissionController::_onRallyPointsAboutToBeRemoved(const QModelIndex& parent, int first, int last) +void MissionController::_syncTreeRallyPointsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { Q_UNUSED(parent); for (int i = last; i >= first; i--) { _visualItemsTree.removeAt(_rallyGroupIndex, i); } +} - // If all rally points are being removed, re-add the header marker +void MissionController::_syncTreeRallyPointsRemoved(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + Q_UNUSED(first); + Q_UNUSED(last); + + // Re-add the header marker once the source model has finished removing rows auto* rallyController = _masterController->rallyPointController(); - if (rallyController && (rallyController->points()->count() - (last - first + 1)) == 0) { + if (rallyController && rallyController->points()->count() == 0) { _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader")); _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader")); } } -void MissionController::_onRallyPointsReset(void) +void MissionController::_syncTreeRallyPointsReset(void) { - // Remove all rally children + // removeChildren + appendItem each fire their own row-level signals scoped + // to _rallyGroupIndex, so persistent indexes for other groups survive. _visualItemsTree.removeChildren(_rallyGroupIndex); // Repopulate — either rally items or the header marker @@ -2048,23 +2066,24 @@ void MissionController::_initAllVisualItems(void) connect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::_updateContainsItems); // Connect for incremental tree model sync - connect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_onMissionItemsInserted); - connect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_onMissionItemsAboutToBeRemoved); - connect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_onMissionItemsReset); + connect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted); + connect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeMissionItemsAboutToBeRemoved); + connect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeMissionItemsReset); // Populate tree's mission group from current _visualItems - _onMissionItemsReset(); + _syncTreeMissionItemsReset(); // Connect rally controller for incremental tree model sync auto* rallyController = _masterController->rallyPointController(); if (rallyController) { auto* pts = rallyController->points(); - connect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_onRallyPointsInserted); - connect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_onRallyPointsAboutToBeRemoved); - connect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_onRallyPointsReset); + connect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeRallyPointsInserted); + connect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeRallyPointsAboutToBeRemoved); + connect(pts, &QAbstractItemModel::rowsRemoved, this, &MissionController::_syncTreeRallyPointsRemoved); + connect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeRallyPointsReset); // Populate any existing rally points - _onRallyPointsReset(); + _syncTreeRallyPointsReset(); } emit visualItemsChanged(); @@ -2080,6 +2099,12 @@ void MissionController::_initAllVisualItems(void) void MissionController::_deinitAllVisualItems(void) { + // Remove mission items from the tree model before their C++ objects are + // scheduled for deletion via deleteLater below. Without this, the TreeView + // delegates still hold references when the objects are destroyed, causing + // null-reference warnings in QML (e.g. "Cannot read property 'masterController' of null"). + _visualItemsTree.removeChildren(_missionGroupIndex); + disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::_recalcAll); disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::plannedHomePositionChanged); @@ -2091,17 +2116,18 @@ void MissionController::_deinitAllVisualItems(void) disconnect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::_updateContainsItems); // Disconnect incremental tree model sync - disconnect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_onMissionItemsInserted); - disconnect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_onMissionItemsAboutToBeRemoved); - disconnect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_onMissionItemsReset); + disconnect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted); + disconnect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeMissionItemsAboutToBeRemoved); + disconnect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeMissionItemsReset); // Disconnect rally controller tree model sync auto* rallyController = _masterController->rallyPointController(); if (rallyController) { auto* pts = rallyController->points(); - disconnect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_onRallyPointsInserted); - disconnect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_onRallyPointsAboutToBeRemoved); - disconnect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_onRallyPointsReset); + disconnect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeRallyPointsInserted); + disconnect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeRallyPointsAboutToBeRemoved); + disconnect(pts, &QAbstractItemModel::rowsRemoved, this, &MissionController::_syncTreeRallyPointsRemoved); + disconnect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeRallyPointsReset); } } diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index e14f8e6ef26b..974ac5e15962 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -391,12 +391,13 @@ private slots: void _managerVehicleChanged (Vehicle* managerVehicle); void _forceRecalcOfAllowedBits (void); // Incremental tree model sync slots - void _onMissionItemsInserted (const QModelIndex& parent, int first, int last); - void _onMissionItemsAboutToBeRemoved (const QModelIndex& parent, int first, int last); - void _onMissionItemsReset (void); - void _onRallyPointsInserted (const QModelIndex& parent, int first, int last); - void _onRallyPointsAboutToBeRemoved (const QModelIndex& parent, int first, int last); - void _onRallyPointsReset (void); + void _syncTreeMissionItemsInserted (const QModelIndex& parent, int first, int last); + void _syncTreeMissionItemsAboutToBeRemoved (const QModelIndex& parent, int first, int last); + void _syncTreeMissionItemsReset (void); + void _syncTreeRallyPointsInserted (const QModelIndex& parent, int first, int last); + void _syncTreeRallyPointsAboutToBeRemoved (const QModelIndex& parent, int first, int last); + void _syncTreeRallyPointsRemoved (const QModelIndex& parent, int first, int last); + void _syncTreeRallyPointsReset (void); private: void _init (void); void _setupTreeModel (void); From 2eada81204aff57d8c88a19f0ca8ab046ec494d3 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 19:11:15 -0700 Subject: [PATCH 103/119] EditPositionDialog: refactor and add altitude support - Remove showSetPositionFromVehicle property; show Vehicle Position option whenever an active vehicle exists - Consolidate four Set position/Move buttons into a single dispatch button - Show vehicle lat/lon and altitude labels in Vehicle Position mode - Add altitudeFact and altitudeFrame properties for optional altitude editing - Show editable altitude field with frame label (Rel/AMSL/AGL) for Geographic, UTM, and MGRS modes when caller provides an altitude Fact - Show frame-relevant vehicle altitude in Vehicle Position mode - Add checkboxes to independently set position and/or altitude from vehicle (position defaults on, altitude defaults off) - Wire MissionItemEditor to pass altitude info for SimpleMissionItems - Remove unused showSetPositionFromVehicle parameter from FlyViewMap --- src/FlyView/FlyViewMap.qml | 2 +- src/PlanView/MissionItemEditor.qml | 18 +++- src/QmlControls/EditPositionDialog.qml | 113 +++++++++++++++++-------- 3 files changed, 96 insertions(+), 37 deletions(-) diff --git a/src/FlyView/FlyViewMap.qml b/src/FlyView/FlyViewMap.qml index dfd7753bf129..eac227e0d1a3 100644 --- a/src/FlyView/FlyViewMap.qml +++ b/src/FlyView/FlyViewMap.qml @@ -651,7 +651,7 @@ FlightMap { Layout.fillWidth: true text: qsTr("Edit Position") onClicked: { - roiEditPositionDialogFactory.open({ showSetPositionFromVehicle: false }) + roiEditPositionDialogFactory.open() roiEditDropPanel.close() } } diff --git a/src/PlanView/MissionItemEditor.qml b/src/PlanView/MissionItemEditor.qml index 450821814c8b..1b9540a64fa0 100644 --- a/src/PlanView/MissionItemEditor.qml +++ b/src/PlanView/MissionItemEditor.qml @@ -89,8 +89,14 @@ Rectangle { id: editPositionDialog EditPositionDialog { - coordinate: missionItem.isSurveyItem ? missionItem.centerCoordinate : missionItem.coordinate - onCoordinateChanged: missionItem.isSurveyItem ? missionItem.centerCoordinate = coordinate : missionItem.coordinate = coordinate + property bool _editCenterCoordinate: false + + onCoordinateChanged: { + if (_editCenterCoordinate) + missionItem.centerCoordinate = coordinate + else + missionItem.coordinate = coordinate + } } } @@ -243,7 +249,13 @@ Rectangle { text: qsTr("Edit position...") enabled: missionItem.specifiesCoordinate onClicked: { - editPositionDialogFactory.open() + const editCenterCoordinate = missionItem.isSurveyItem + editPositionDialogFactory.open({ + _editCenterCoordinate: editCenterCoordinate, + coordinate: editCenterCoordinate ? missionItem.centerCoordinate : missionItem.coordinate, + altitudeFact: !editCenterCoordinate && missionItem.specifiesAltitude ? missionItem.altitude : null, + altitudeFrame: !editCenterCoordinate && missionItem.specifiesAltitude ? missionItem.altitudeFrame : QGroundControl.AltitudeFrameNone, + }) hamburgerMenuDropPanel.close() } } diff --git a/src/QmlControls/EditPositionDialog.qml b/src/QmlControls/EditPositionDialog.qml index 2cf7cdf31d3f..479225c6d725 100644 --- a/src/QmlControls/EditPositionDialog.qml +++ b/src/QmlControls/EditPositionDialog.qml @@ -12,8 +12,9 @@ QGCPopupDialog { title: qsTr("Edit Position") buttons: Dialog.Close - property alias coordinate: controller.coordinate - property bool showSetPositionFromVehicle: true + property alias coordinate: controller.coordinate + property var altitudeFact: null + property int altitudeFrame: QGroundControl.AltitudeFrameNone property real _margin: ScreenTools.defaultFontPixelWidth / 2 property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 20 @@ -21,6 +22,12 @@ QGCPopupDialog { property bool _showUTM: coordinateSystemCombo.comboBox.currentIndex === 1 property bool _showMGRS: coordinateSystemCombo.comboBox.currentIndex === 2 property bool _showVehicle: coordinateSystemCombo.comboBox.currentIndex === 3 + property bool _supportsAltitude: altitudeFact !== null + + function _isFiniteAltitude(value) { + const numericValue = Number(value) + return !isNaN(numericValue) && isFinite(numericValue) + } TransformPositionController { id: controller @@ -35,7 +42,7 @@ QGCPopupDialog { id: coordinateSystemCombo Layout.fillWidth: true label: qsTr("Coordinate System") - model: showSetPositionFromVehicle && globals.activeVehicle ? + model: globals.activeVehicle ? [ qsTr("Geographic"), qsTr("Universal Transverse Mercator"), qsTr("Military Grid Reference"), qsTr("Vehicle Position") ] : [ qsTr("Geographic"), qsTr("Universal Transverse Mercator"), qsTr("Military Grid Reference") ] } @@ -56,16 +63,6 @@ QGCPopupDialog { visible: _showGeographic } - LabelledButton { - label: qsTr("Set position") - buttonText: qsTr("Move") - visible: _showGeographic - onClicked: { - controller.setFromGeo() - root.close() - } - } - LabelledFactTextField { label: qsTr("Zone") fact: controller.zone @@ -98,16 +95,6 @@ QGCPopupDialog { visible: _showUTM } - LabelledButton { - label: qsTr("Set position") - buttonText: qsTr("Move") - visible: _showUTM - onClicked: { - controller.setFromUTM() - root.close() - } - } - LabelledFactTextField { label: qsTr("MGRS") fact: controller.mgrs @@ -116,22 +103,82 @@ QGCPopupDialog { Layout.fillWidth: true } - LabelledButton { - label: qsTr("Set position") - buttonText: qsTr("Move") - visible: _showMGRS - onClicked: { - controller.setFromMGRS() - root.close() - } + LabelledLabel { + label: qsTr("Latitude") + labelText: globals.activeVehicle ? globals.activeVehicle.coordinate.latitude.toFixed(7) : "" + Layout.fillWidth: true + visible: _showVehicle + } + + LabelledLabel { + label: qsTr("Longitude") + labelText: globals.activeVehicle ? globals.activeVehicle.coordinate.longitude.toFixed(7) : "" + Layout.fillWidth: true + visible: _showVehicle + } + + LabelledLabel { + label: qsTr("Alt (AMSL)") + labelText: globals.activeVehicle ? globals.activeVehicle.altitudeAMSL.valueString + " " + globals.activeVehicle.altitudeAMSL.units : "" + Layout.fillWidth: true + visible: _showVehicle && _supportsAltitude && altitudeFrame === QGroundControl.AltitudeFrameAbsolute + } + + LabelledLabel { + label: qsTr("Alt (Rel)") + labelText: globals.activeVehicle ? globals.activeVehicle.altitudeRelative.valueString + " " + globals.activeVehicle.altitudeRelative.units : "" + Layout.fillWidth: true + visible: _showVehicle && _supportsAltitude && altitudeFrame === QGroundControl.AltitudeFrameRelative + } + + LabelledLabel { + label: qsTr("Alt (AGL)") + labelText: globals.activeVehicle ? globals.activeVehicle.altitudeAboveTerr.valueString + " " + globals.activeVehicle.altitudeAboveTerr.units : "" + Layout.fillWidth: true + visible: _showVehicle && _supportsAltitude && (altitudeFrame === QGroundControl.AltitudeFrameTerrain || altitudeFrame === QGroundControl.AltitudeFrameCalcAboveTerrain) + } + + QGCCheckBox { + id: setPositionCheckBox + text: qsTr("Set position from vehicle") + checked: true + visible: _showVehicle + } + + QGCCheckBox { + id: setAltitudeCheckBox + text: qsTr("Set altitude from vehicle") + checked: false + visible: _showVehicle && _supportsAltitude } LabelledButton { label: qsTr("Set position") buttonText: qsTr("Move") - visible: _showVehicle onClicked: { - controller.setFromVehicle() + if (_showGeographic) + controller.setFromGeo() + else if (_showUTM) + controller.setFromUTM() + else if (_showMGRS) + controller.setFromMGRS() + else if (_showVehicle) { + if (setPositionCheckBox.checked) + controller.setFromVehicle() + if (setAltitudeCheckBox.checked && _supportsAltitude && globals.activeVehicle) { + let sourceAltitude = NaN + + if (altitudeFrame === QGroundControl.AltitudeFrameRelative) + sourceAltitude = globals.activeVehicle.altitudeRelative.rawValue + else if (altitudeFrame === QGroundControl.AltitudeFrameAbsolute) + sourceAltitude = globals.activeVehicle.altitudeAMSL.rawValue + else if (altitudeFrame === QGroundControl.AltitudeFrameTerrain || altitudeFrame === QGroundControl.AltitudeFrameCalcAboveTerrain) + sourceAltitude = globals.activeVehicle.altitudeAboveTerr.rawValue + + if (_isFiniteAltitude(sourceAltitude)) + altitudeFact.rawValue = sourceAltitude + } + } root.close() } } From 973c9afb9f65efe54849f851ce78cf65cef58d17 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 17:18:01 -0700 Subject: [PATCH 104/119] FlyView: Add left margin to MapScale from toolstrip Adds a left margin (_toolsMargin) to the MapScale widget so it has proper spacing from the FlyViewToolStrip instead of being flush against it. Also removes unused centerInset property from the pip-mode MapScale in FlyViewMap. Fixes #13526 --- src/FlyView/FlyViewMap.qml | 3 --- src/FlyView/FlyViewWidgetLayer.qml | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/FlyView/FlyViewMap.qml b/src/FlyView/FlyViewMap.qml index eac227e0d1a3..d6eacc939377 100644 --- a/src/FlyView/FlyViewMap.qml +++ b/src/FlyView/FlyViewMap.qml @@ -775,8 +775,5 @@ FlightMap { anchors.top: parent.top mapControl: _root visible: !ScreenTools.isTinyScreen && QGroundControl.corePlugin.options.flyView.showMapScale && mapControl.pipState.state === mapControl.pipState.windowState - - property real centerInset: visible ? parent.height - y : 0 } - } diff --git a/src/FlyView/FlyViewWidgetLayer.qml b/src/FlyView/FlyViewWidgetLayer.qml index b18a2f8bd85c..f79499a123d7 100644 --- a/src/FlyView/FlyViewWidgetLayer.qml +++ b/src/FlyView/FlyViewWidgetLayer.qml @@ -166,6 +166,7 @@ Item { MapScale { id: mapScale anchors.left: toolStrip.right + anchors.leftMargin: _toolsMargin anchors.top: parent.top mapControl: _mapControl autoHide: true From a7476b29ba3de9a231240b8b859a0ba692e1c5d3 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 16:11:10 -0700 Subject: [PATCH 105/119] Fix PID tuning sliders showing wrong params for each axis The nested Repeaters in PIDTuning.qml had an index shadowing bug. The inner Repeater's delegate used 'index' for the visibility check, but inside the inner Repeater 'index' refers to the parameter index rather than the outer axis index. This caused each axis tab to show one wrong parameter from across all axes instead of all parameters for the selected axis. Store the outer axis index in a property on the inner Repeater and reference that property in the visibility binding. Fixes #13351 --- src/QmlControls/PIDTuning.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/QmlControls/PIDTuning.qml b/src/QmlControls/PIDTuning.qml index 3f186ce1427f..bbb4091926bb 100644 --- a/src/QmlControls/PIDTuning.qml +++ b/src/QmlControls/PIDTuning.qml @@ -333,13 +333,16 @@ RowLayout { model: axis Repeater { + id: paramRepeater model: axis[index].params + property int axisIndex: index + SettingsGroupLayout { id: tuningGroup heading: title headingDescription: description - visible: _currentAxis === index + visible: _currentAxis === paramRepeater.axisIndex Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 40 FactSlider { From 7d3613d0fc428f2d8684c3e4b8ca603ab47112f9 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 20:52:35 -0700 Subject: [PATCH 106/119] Rename MAVLinkLog to OnboardLog in AnalyzeView Rename the onboard log download feature from MAVLinkLog to OnboardLog to better reflect its purpose. This affects the AnalyzeView log list and download functionality, not the MAVLink telemetry log streaming in Vehicle/MAVLinkLogManager. - Rename directory: MAVLinkLogs -> OnboardLogs - Rename classes: MAVLinkLogController -> OnboardLogController, QGCMAVLinkLogEntry -> QGCOnboardLogEntry, MAVLinkLogDownloadData -> OnboardLogDownloadData - Update all user-facing strings - Update test files and registration Fixes #14137 --- qgcimages.qrc | 2 +- src/API/QGCCorePlugin.cc | 6 +- src/AnalyzeView/CMakeLists.txt | 4 +- .../CMakeLists.txt | 12 +- .../OnboardLogController.cc} | 168 +++++++++--------- .../OnboardLogController.h} | 20 +-- .../OnboardLogEntry.cc} | 30 ++-- .../OnboardLogEntry.h} | 18 +- .../OnboardLogIcon.svg} | 0 .../OnboardLogPage.qml} | 50 +++--- test/AnalyzeView/CMakeLists.txt | 4 +- ...nloadTest.cc => OnboardLogDownloadTest.cc} | 14 +- ...ownloadTest.h => OnboardLogDownloadTest.h} | 2 +- test/CMakeLists.txt | 2 +- 14 files changed, 166 insertions(+), 166 deletions(-) rename src/AnalyzeView/{MAVLinkLogs => OnboardLogs}/CMakeLists.txt (61%) rename src/AnalyzeView/{MAVLinkLogs/MAVLinkLogController.cc => OnboardLogs/OnboardLogController.cc} (73%) rename src/AnalyzeView/{MAVLinkLogs/MAVLinkLogController.h => OnboardLogs/OnboardLogController.h} (89%) rename src/AnalyzeView/{MAVLinkLogs/MAVLinkLogEntry.cc => OnboardLogs/OnboardLogEntry.cc} (54%) rename src/AnalyzeView/{MAVLinkLogs/MAVLinkLogEntry.h => OnboardLogs/OnboardLogEntry.h} (89%) rename src/AnalyzeView/{MAVLinkLogs/MAVLinkLogIcon.svg => OnboardLogs/OnboardLogIcon.svg} (100%) rename src/AnalyzeView/{MAVLinkLogs/MAVLinkLogPage.qml => OnboardLogs/OnboardLogPage.qml} (70%) rename test/AnalyzeView/{MAVLinkLogDownloadTest.cc => OnboardLogDownloadTest.cc} (83%) rename test/AnalyzeView/{MAVLinkLogDownloadTest.h => OnboardLogDownloadTest.h} (69%) diff --git a/qgcimages.qrc b/qgcimages.qrc index 1db8eea91c28..cb92fc65167f 100644 --- a/qgcimages.qrc +++ b/qgcimages.qrc @@ -115,7 +115,7 @@ src/AutoPilotPlugins/PX4/Images/LandMode.svg src/AutoPilotPlugins/PX4/Images/LandModeCopter.svg src/AutoPilotPlugins/APM/Images/LightsComponentIcon.png - src/AnalyzeView/MAVLinkLogs/MAVLinkLogIcon.svg + src/AnalyzeView/OnboardLogs/OnboardLogIcon.svg src/AutoPilotPlugins/PX4/Images/LowBattery.svg src/AutoPilotPlugins/PX4/Images/LowBatteryLight.svg src/FlightMap/Images/MapAddMission.svg diff --git a/src/API/QGCCorePlugin.cc b/src/API/QGCCorePlugin.cc index 06b55ab0e35a..90347d290b48 100644 --- a/src/API/QGCCorePlugin.cc +++ b/src/API/QGCCorePlugin.cc @@ -63,9 +63,9 @@ const QVariantList &QGCCorePlugin::analyzePages() { static const QVariantList analyzeList = { QVariant::fromValue(new QmlComponentInfo( - tr("MAVLink Log"), - QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkLogs/MAVLinkLogPage.qml")), - QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/MAVLinkLogIcon.svg")))), + tr("Onboard Logs"), + QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/OnboardLogs/OnboardLogPage.qml")), + QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/OnboardLogIcon.svg")))), QVariant::fromValue(new QmlComponentInfo( tr("GeoTag Images"), QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/GeoTag/GeoTagPage.qml")), diff --git a/src/AnalyzeView/CMakeLists.txt b/src/AnalyzeView/CMakeLists.txt index 42511311a0a0..0969a76c349d 100644 --- a/src/AnalyzeView/CMakeLists.txt +++ b/src/AnalyzeView/CMakeLists.txt @@ -6,7 +6,7 @@ add_subdirectory(GeoTag) add_subdirectory(MAVLinkConsole) add_subdirectory(MAVLinkInspector) -add_subdirectory(MAVLinkLogs) +add_subdirectory(OnboardLogs) target_sources(${CMAKE_PROJECT_NAME} PRIVATE @@ -30,7 +30,7 @@ qt_add_qml_module(AnalyzeViewModule MAVLinkInspector/MAVLinkChart.qml MAVLinkInspector/MAVLinkMessageButton.qml MAVLinkInspector/MAVLinkInspectorPage.qml - MAVLinkLogs/MAVLinkLogPage.qml + OnboardLogs/OnboardLogPage.qml Vibration/VibrationPage.qml NO_PLUGIN ) diff --git a/src/AnalyzeView/MAVLinkLogs/CMakeLists.txt b/src/AnalyzeView/OnboardLogs/CMakeLists.txt similarity index 61% rename from src/AnalyzeView/MAVLinkLogs/CMakeLists.txt rename to src/AnalyzeView/OnboardLogs/CMakeLists.txt index 6c378db82e03..f47679afa27c 100644 --- a/src/AnalyzeView/MAVLinkLogs/CMakeLists.txt +++ b/src/AnalyzeView/OnboardLogs/CMakeLists.txt @@ -1,14 +1,14 @@ # ============================================================================ -# MAVLink Logs Module -# MAVLink log list and download functionality +# Onboard Logs Module +# Onboard log list and download functionality # ============================================================================ target_sources(${CMAKE_PROJECT_NAME} PRIVATE - MAVLinkLogController.cc - MAVLinkLogController.h - MAVLinkLogEntry.cc - MAVLinkLogEntry.h + OnboardLogController.cc + OnboardLogController.h + OnboardLogEntry.cc + OnboardLogEntry.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.cc b/src/AnalyzeView/OnboardLogs/OnboardLogController.cc similarity index 73% rename from src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.cc rename to src/AnalyzeView/OnboardLogs/OnboardLogController.cc index b3ffd7f083e8..f6b9638bb6b9 100644 --- a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.cc +++ b/src/AnalyzeView/OnboardLogs/OnboardLogController.cc @@ -1,6 +1,6 @@ -#include "MAVLinkLogController.h" +#include "OnboardLogController.h" #include "AppSettings.h" -#include "MAVLinkLogEntry.h" +#include "OnboardLogEntry.h" #include "MAVLinkProtocol.h" #include "MultiVehicleManager.h" #include "ParameterManager.h" @@ -13,35 +13,35 @@ #include #include -QGC_LOGGING_CATEGORY(MAVLinkLogControllerLog, "AnalyzeView.MAVLinkLogController") +QGC_LOGGING_CATEGORY(OnboardLogControllerLog, "AnalyzeView.OnboardLogController") -MAVLinkLogController::MAVLinkLogController(QObject *parent) +OnboardLogController::OnboardLogController(QObject *parent) : QObject(parent) , _timer(new QTimer(this)) , _logEntriesModel(new QmlObjectListModel(this)) { - qCDebug(MAVLinkLogControllerLog) << this; + qCDebug(OnboardLogControllerLog) << this; - (void) connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &MAVLinkLogController::_setActiveVehicle); - (void) connect(_timer, &QTimer::timeout, this, &MAVLinkLogController::_processDownload); + (void) connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &OnboardLogController::_setActiveVehicle); + (void) connect(_timer, &QTimer::timeout, this, &OnboardLogController::_processDownload); _timer->setSingleShot(false); _setActiveVehicle(MultiVehicleManager::instance()->activeVehicle()); } -MAVLinkLogController::~MAVLinkLogController() +OnboardLogController::~OnboardLogController() { - qCDebug(MAVLinkLogControllerLog) << this; + qCDebug(OnboardLogControllerLog) << this; } -void MAVLinkLogController::download(const QString &path) +void OnboardLogController::download(const QString &path) { const QString dir = path.isEmpty() ? SettingsManager::instance()->appSettings()->logSavePath() : path; _downloadToDirectory(dir); } -void MAVLinkLogController::_downloadToDirectory(const QString &dir) +void OnboardLogController::_downloadToDirectory(const QString &dir) { _receivedAllEntries(); @@ -56,7 +56,7 @@ void MAVLinkLogController::_downloadToDirectory(const QString &dir) _downloadPath += QDir::separator(); } - QGCMAVLinkLogEntry *const log = _getNextSelected(); + QGCOnboardLogEntry *const log = _getNextSelected(); if (log) { log->setStatus(tr("Waiting")); } @@ -65,7 +65,7 @@ void MAVLinkLogController::_downloadToDirectory(const QString &dir) _receivedAllData(); } -void MAVLinkLogController::_processDownload() +void OnboardLogController::_processDownload() { if (_requestingLogEntries) { _findMissingEntries(); @@ -74,13 +74,13 @@ void MAVLinkLogController::_processDownload() } } -void MAVLinkLogController::_findMissingEntries() +void OnboardLogController::_findMissingEntries() { const int num_logs = _logEntriesModel->count(); int start = -1; int end = -1; for (int i = 0; i < num_logs; i++) { - const QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); + const QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -103,14 +103,14 @@ void MAVLinkLogController::_findMissingEntries() if (_retries++ > 2) { for (int i = 0; i < num_logs; i++) { - QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); + QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (entry && !entry->received()) { entry->setStatus(tr("Error")); } } _receivedAllEntries(); - qCWarning(MAVLinkLogControllerLog) << "Too many errors retreiving log list. Giving up."; + qCWarning(OnboardLogControllerLog) << "Too many errors retreiving log list. Giving up."; return; } @@ -124,7 +124,7 @@ void MAVLinkLogController::_findMissingEntries() _requestLogList(static_cast(start), static_cast(end)); } -void MAVLinkLogController::_setActiveVehicle(Vehicle *vehicle) +void OnboardLogController::_setActiveVehicle(Vehicle *vehicle) { if (vehicle == _vehicle) { return; @@ -132,19 +132,19 @@ void MAVLinkLogController::_setActiveVehicle(Vehicle *vehicle) if (_vehicle) { _logEntriesModel->clearAndDeleteContents(); - (void) disconnect(_vehicle, &Vehicle::logEntry, this, &MAVLinkLogController::_logEntry); - (void) disconnect(_vehicle, &Vehicle::logData, this, &MAVLinkLogController::_logData); + (void) disconnect(_vehicle, &Vehicle::logEntry, this, &OnboardLogController::_logEntry); + (void) disconnect(_vehicle, &Vehicle::logData, this, &OnboardLogController::_logData); } _vehicle = vehicle; if (_vehicle) { - (void) connect(_vehicle, &Vehicle::logEntry, this, &MAVLinkLogController::_logEntry); - (void) connect(_vehicle, &Vehicle::logData, this, &MAVLinkLogController::_logData); + (void) connect(_vehicle, &Vehicle::logEntry, this, &OnboardLogController::_logEntry); + (void) connect(_vehicle, &Vehicle::logData, this, &OnboardLogController::_logData); } } -void MAVLinkLogController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t id, uint16_t num_logs, uint16_t last_log_num) +void OnboardLogController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t id, uint16_t num_logs, uint16_t last_log_num) { Q_UNUSED(last_log_num); @@ -159,7 +159,7 @@ void MAVLinkLogController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t } for (int i = 0; i < num_logs; i++) { - QGCMAVLinkLogEntry *const entry = new QGCMAVLinkLogEntry(i); + QGCOnboardLogEntry *const entry = new QGCOnboardLogEntry(i); _logEntriesModel->append(entry); } } @@ -168,13 +168,13 @@ void MAVLinkLogController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t if ((size > 0) || (_vehicle->firmwareType() != MAV_AUTOPILOT_ARDUPILOTMEGA)) { id -= _apmOffset; if (id < _logEntriesModel->count()) { - QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(id); + QGCOnboardLogEntry *const entry = _logEntriesModel->value(id); entry->setSize(size); entry->setTime(QDateTime::fromSecsSinceEpoch(time_utc)); entry->setReceived(true); entry->setStatus(tr("Available")); } else { - qCWarning(MAVLinkLogControllerLog) << "Received MAVLink log entry for out-of-bound index:" << id; + qCWarning(OnboardLogControllerLog) << "Received onboard log entry for out-of-bound index:" << id; } } } else { @@ -190,17 +190,17 @@ void MAVLinkLogController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t } } -void MAVLinkLogController::_receivedAllEntries() +void OnboardLogController::_receivedAllEntries() { _timer->stop(); _setListing(false); } -bool MAVLinkLogController::_entriesComplete() const +bool OnboardLogController::_entriesComplete() const { const int num_logs = _logEntriesModel->count(); for (int i = 0; i < num_logs; i++) { - const QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); + const QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -213,7 +213,7 @@ bool MAVLinkLogController::_entriesComplete() const return true; } -void MAVLinkLogController::_logData(uint32_t ofs, uint16_t id, uint8_t count, const uint8_t *data) +void OnboardLogController::_logData(uint32_t ofs, uint16_t id, uint8_t count, const uint8_t *data) { if (!_downloadingLogs || !_downloadData) { return; @@ -221,34 +221,34 @@ void MAVLinkLogController::_logData(uint32_t ofs, uint16_t id, uint8_t count, co id -= _apmOffset; if (_downloadData->ID != id) { - qCWarning(MAVLinkLogControllerLog) << "Received log data for wrong log"; + qCWarning(OnboardLogControllerLog) << "Received log data for wrong log"; return; } if ((ofs % MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN) != 0) { - qCWarning(MAVLinkLogControllerLog) << "Ignored misaligned incoming packet @" << ofs; + qCWarning(OnboardLogControllerLog) << "Ignored misaligned incoming packet @" << ofs; return; } bool result = false; if (ofs <= _downloadData->entry->size()) { - const uint32_t chunk = ofs / MAVLinkLogDownloadData::kChunkSize; - // qCDebug(MAVLinkLogControllerLog) << "Received data - Offset:" << ofs << "Chunk:" << chunk; + const uint32_t chunk = ofs / OnboardLogDownloadData::kChunkSize; + // qCDebug(OnboardLogControllerLog) << "Received data - Offset:" << ofs << "Chunk:" << chunk; if (chunk != _downloadData->current_chunk) { - qCWarning(MAVLinkLogControllerLog) << "Ignored packet for out of order chunk actual:expected" << chunk << _downloadData->current_chunk; + qCWarning(OnboardLogControllerLog) << "Ignored packet for out of order chunk actual:expected" << chunk << _downloadData->current_chunk; return; } - const uint16_t bin = (ofs - (chunk * MAVLinkLogDownloadData::kChunkSize)) / MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; + const uint16_t bin = (ofs - (chunk * OnboardLogDownloadData::kChunkSize)) / MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; if (bin >= _downloadData->chunk_table.size()) { - qCWarning(MAVLinkLogControllerLog) << "Out of range bin received"; + qCWarning(OnboardLogControllerLog) << "Out of range bin received"; } else { _downloadData->chunk_table.setBit(bin); } if (_downloadData->file.pos() != ofs) { if (!_downloadData->file.seek(ofs)) { - qCWarning(MAVLinkLogControllerLog) << "Error while seeking log file offset"; + qCWarning(OnboardLogControllerLog) << "Error while seeking log file offset"; return; } } @@ -268,17 +268,17 @@ void MAVLinkLogController::_logData(uint32_t ofs, uint16_t id, uint8_t count, co } else if (_chunkComplete()) { _downloadData->advanceChunk(); _requestLogData(_downloadData->ID, - _downloadData->current_chunk * MAVLinkLogDownloadData::kChunkSize, + _downloadData->current_chunk * OnboardLogDownloadData::kChunkSize, _downloadData->chunk_table.size() * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); } else if ((bin < (_downloadData->chunk_table.size() - 1)) && _downloadData->chunk_table.at(bin + 1)) { // Likely to be grabbing fragments and got to the end of a gap _findMissingData(); } } else { - qCWarning(MAVLinkLogControllerLog) << "Error while writing log file chunk"; + qCWarning(OnboardLogControllerLog) << "Error while writing log file chunk"; } } else { - qCWarning(MAVLinkLogControllerLog) << "Received log offset greater than expected"; + qCWarning(OnboardLogControllerLog) << "Received log offset greater than expected"; } if (!result) { @@ -286,7 +286,7 @@ void MAVLinkLogController::_logData(uint32_t ofs, uint16_t id, uint8_t count, co } } -void MAVLinkLogController::_findMissingData() +void OnboardLogController::_findMissingData() { if (_logComplete()) { _receivedAllData(); @@ -315,12 +315,12 @@ void MAVLinkLogController::_findMissingData() } } - const uint32_t pos = (_downloadData->current_chunk * MAVLinkLogDownloadData::kChunkSize) + (start * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); + const uint32_t pos = (_downloadData->current_chunk * OnboardLogDownloadData::kChunkSize) + (start * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); const uint32_t len = (end - start) * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; _requestLogData(_downloadData->ID, pos, len, _retries); } -void MAVLinkLogController::_updateDataRate() +void OnboardLogController::_updateDataRate() { constexpr uint kSizeUpdateThreshold = 102400; // 0.1 MB const bool timeThresholdMet = _downloadData->elapsed.elapsed() >= kGUIRateMs; @@ -350,17 +350,17 @@ void MAVLinkLogController::_updateDataRate() _downloadData->last_status_written = _downloadData->written; } -bool MAVLinkLogController::_chunkComplete() const +bool OnboardLogController::_chunkComplete() const { return _downloadData->chunkEquals(true); } -bool MAVLinkLogController::_logComplete() const +bool OnboardLogController::_logComplete() const { return (_chunkComplete() && ((_downloadData->current_chunk + 1) == _downloadData->numChunks())); } -void MAVLinkLogController::_receivedAllData() +void OnboardLogController::_receivedAllData() { _timer->stop(); if (_prepareLogDownload()) { @@ -372,11 +372,11 @@ void MAVLinkLogController::_receivedAllData() } } -bool MAVLinkLogController::_prepareLogDownload() +bool OnboardLogController::_prepareLogDownload() { _downloadData.reset(); - QGCMAVLinkLogEntry *const entry = _getNextSelected(); + QGCOnboardLogEntry *const entry = _getNextSelected(); if (!entry) { return false; } @@ -386,7 +386,7 @@ bool MAVLinkLogController::_prepareLogDownload() const QString ftime = (entry->time().date().year() >= 2010) ? entry->time().toString(QStringLiteral("yyyy-M-d-hh-mm-ss")) : QStringLiteral("UnknownDate"); - _downloadData = std::make_unique(entry); + _downloadData = std::make_unique(entry); _downloadData->filename = QStringLiteral("log_") + QString::number(entry->id()) + "_" + ftime; if (_vehicle->firmwareType() == MAV_AUTOPILOT_PX4) { @@ -415,9 +415,9 @@ bool MAVLinkLogController::_prepareLogDownload() bool result = false; if (!_downloadData->file.open(QIODevice::WriteOnly)) { - qCWarning(MAVLinkLogControllerLog) << "Failed to create log file:" << _downloadData->filename; + qCWarning(OnboardLogControllerLog) << "Failed to create log file:" << _downloadData->filename; } else if (!_downloadData->file.resize(entry->size())) { - qCWarning(MAVLinkLogControllerLog) << "Failed to allocate space for log file:" << _downloadData->filename; + qCWarning(OnboardLogControllerLog) << "Failed to allocate space for log file:" << _downloadData->filename; } else { _downloadData->current_chunk = 0; _downloadData->chunk_table = QBitArray(_downloadData->chunkBins(), false); @@ -437,17 +437,17 @@ bool MAVLinkLogController::_prepareLogDownload() return result; } -void MAVLinkLogController::refresh() +void OnboardLogController::refresh() { _logEntriesModel->clearAndDeleteContents(); _requestLogList(0, 0xffff); } -QGCMAVLinkLogEntry *MAVLinkLogController::_getNextSelected() const +QGCOnboardLogEntry *OnboardLogController::_getNextSelected() const { const int numLogs = _logEntriesModel->count(); for (int i = 0; i < numLogs; i++) { - QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); + QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -460,7 +460,7 @@ QGCMAVLinkLogEntry *MAVLinkLogController::_getNextSelected() const return nullptr; } -void MAVLinkLogController::cancel() +void OnboardLogController::cancel() { _requestLogEnd(); _receivedAllEntries(); @@ -478,11 +478,11 @@ void MAVLinkLogController::cancel() _setDownloading(false); } -void MAVLinkLogController::_resetSelection(bool canceled) +void OnboardLogController::_resetSelection(bool canceled) { const int num_logs = _logEntriesModel->count(); for (int i = 0; i < num_logs; i++) { - QGCMAVLinkLogEntry *const entry = _logEntriesModel->value(i); + QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -498,16 +498,16 @@ void MAVLinkLogController::_resetSelection(bool canceled) emit selectionChanged(); } -void MAVLinkLogController::eraseAll() +void OnboardLogController::eraseAll() { if (!_vehicle) { - qCWarning(MAVLinkLogControllerLog) << "Vehicle Unavailable"; + qCWarning(OnboardLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(MAVLinkLogControllerLog) << "Link Unavailable"; + qCWarning(OnboardLogControllerLog) << "Link Unavailable"; return; } @@ -522,23 +522,23 @@ void MAVLinkLogController::eraseAll() ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(MAVLinkLogControllerLog) << "Failed to send"; + qCWarning(OnboardLogControllerLog) << "Failed to send"; return; } refresh(); } -void MAVLinkLogController::_requestLogList(uint32_t start, uint32_t end) +void OnboardLogController::_requestLogList(uint32_t start, uint32_t end) { if (!_vehicle) { - qCWarning(MAVLinkLogControllerLog) << "Vehicle Unavailable"; + qCWarning(OnboardLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(MAVLinkLogControllerLog) << "Link Unavailable"; + qCWarning(OnboardLogControllerLog) << "Link Unavailable"; return; } @@ -555,30 +555,30 @@ void MAVLinkLogController::_requestLogList(uint32_t start, uint32_t end) ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(MAVLinkLogControllerLog) << "Failed to send"; + qCWarning(OnboardLogControllerLog) << "Failed to send"; return; } - qCDebug(MAVLinkLogControllerLog) << "Request MAVLink log entry list (" << start << "through" << end << ")"; + qCDebug(OnboardLogControllerLog) << "Request onboard log entry list (" << start << "through" << end << ")"; _setListing(true); _timer->start(kRequestLogListTimeoutMs); } -void MAVLinkLogController::_requestLogData(uint16_t id, uint32_t offset, uint32_t count, int retryCount) +void OnboardLogController::_requestLogData(uint16_t id, uint32_t offset, uint32_t count, int retryCount) { if (!_vehicle) { - qCWarning(MAVLinkLogControllerLog) << "Vehicle Unavailable"; + qCWarning(OnboardLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(MAVLinkLogControllerLog) << "Link Unavailable"; + qCWarning(OnboardLogControllerLog) << "Link Unavailable"; return; } id += _apmOffset; - qCDebug(MAVLinkLogControllerLog) << "Request log data (id:" << id << "offset:" << offset << "size:" << count << "retryCount" << retryCount << ")"; + qCDebug(OnboardLogControllerLog) << "Request log data (id:" << id << "offset:" << offset << "size:" << count << "retryCount" << retryCount << ")"; mavlink_message_t msg{}; (void) mavlink_msg_log_request_data_pack_chan( @@ -594,20 +594,20 @@ void MAVLinkLogController::_requestLogData(uint16_t id, uint32_t offset, uint32_ ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(MAVLinkLogControllerLog) << "Failed to send"; + qCWarning(OnboardLogControllerLog) << "Failed to send"; } } -void MAVLinkLogController::_requestLogEnd() +void OnboardLogController::_requestLogEnd() { if (!_vehicle) { - qCWarning(MAVLinkLogControllerLog) << "Vehicle Unavailable"; + qCWarning(OnboardLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(MAVLinkLogControllerLog) << "Link Unavailable"; + qCWarning(OnboardLogControllerLog) << "Link Unavailable"; return; } @@ -622,11 +622,11 @@ void MAVLinkLogController::_requestLogEnd() ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(MAVLinkLogControllerLog) << "Failed to send"; + qCWarning(OnboardLogControllerLog) << "Failed to send"; } } -void MAVLinkLogController::_setDownloading(bool active) +void OnboardLogController::_setDownloading(bool active) { if (_downloadingLogs != active) { _downloadingLogs = active; @@ -635,7 +635,7 @@ void MAVLinkLogController::_setDownloading(bool active) } } -void MAVLinkLogController::_setListing(bool active) +void OnboardLogController::_setListing(bool active) { if (_requestingLogEntries != active) { _requestingLogEntries = active; @@ -644,7 +644,7 @@ void MAVLinkLogController::_setListing(bool active) } } -void MAVLinkLogController::setCompressLogs(bool compress) +void OnboardLogController::setCompressLogs(bool compress) { if (_compressLogs != compress) { _compressLogs = compress; @@ -652,25 +652,25 @@ void MAVLinkLogController::setCompressLogs(bool compress) } } -bool MAVLinkLogController::compressLogFile(const QString &logPath) +bool OnboardLogController::compressLogFile(const QString &logPath) { Q_UNUSED(logPath) - qCWarning(MAVLinkLogControllerLog) << "Log compression not yet implemented (decompression-only API)"; + qCWarning(OnboardLogControllerLog) << "Log compression not yet implemented (decompression-only API)"; return false; } -void MAVLinkLogController::cancelCompression() +void OnboardLogController::cancelCompression() { // Not implemented - compression API is decompression-only } -void MAVLinkLogController::_handleCompressionProgress(qreal progress) +void OnboardLogController::_handleCompressionProgress(qreal progress) { Q_UNUSED(progress) // Not implemented - compression API is decompression-only } -void MAVLinkLogController::_handleCompressionFinished(bool success) +void OnboardLogController::_handleCompressionFinished(bool success) { Q_UNUSED(success) // Not implemented - compression API is decompression-only diff --git a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.h b/src/AnalyzeView/OnboardLogs/OnboardLogController.h similarity index 89% rename from src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.h rename to src/AnalyzeView/OnboardLogs/OnboardLogController.h index 4bfc18022324..0bf1cf74dd60 100644 --- a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogController.h +++ b/src/AnalyzeView/OnboardLogs/OnboardLogController.h @@ -4,17 +4,17 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(MAVLinkLogControllerLog) +Q_DECLARE_LOGGING_CATEGORY(OnboardLogControllerLog) -struct MAVLinkLogDownloadData; -class QGCMAVLinkLogEntry; +struct OnboardLogDownloadData; +class QGCOnboardLogEntry; class QmlObjectListModel; class QTimer; class QThread; class Vehicle; -class MAVLinkLogDownloadTest; +class OnboardLogDownloadTest; -class MAVLinkLogController : public QObject +class OnboardLogController : public QObject { Q_OBJECT QML_ELEMENT @@ -28,11 +28,11 @@ class MAVLinkLogController : public QObject Q_PROPERTY(bool compressing READ compressing NOTIFY compressingChanged) Q_PROPERTY(float compressionProgress READ compressionProgress NOTIFY compressionProgressChanged) - friend class MAVLinkLogDownloadTest; + friend class OnboardLogDownloadTest; public: - explicit MAVLinkLogController(QObject *parent = nullptr); - ~MAVLinkLogController(); + explicit OnboardLogController(QObject *parent = nullptr); + ~OnboardLogController(); Q_INVOKABLE void refresh(); Q_INVOKABLE void download(const QString &path = QString()); @@ -89,7 +89,7 @@ private slots: void _setListing(bool active); void _updateDataRate(); - QGCMAVLinkLogEntry *_getNextSelected() const; + QGCOnboardLogEntry *_getNextSelected() const; QTimer *_timer = nullptr; QmlObjectListModel *_logEntriesModel = nullptr; @@ -98,7 +98,7 @@ private slots: bool _requestingLogEntries = false; int _apmOffset = 0; int _retries = 0; - std::unique_ptr _downloadData; + std::unique_ptr _downloadData; QString _downloadPath; Vehicle *_vehicle = nullptr; bool _compressLogs = false; diff --git a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.cc b/src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc similarity index 54% rename from src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.cc rename to src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc index b2b64dcfe1ed..e5125030dbb6 100644 --- a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.cc +++ b/src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc @@ -1,64 +1,64 @@ -#include "MAVLinkLogEntry.h" +#include "OnboardLogEntry.h" #include "QGCApplication.h" #include "QGCLoggingCategory.h" #include -QGC_LOGGING_CATEGORY(MAVLinkLogEntryLog, "AnalyzeView.QGCMAVLinkLogEntry") +QGC_LOGGING_CATEGORY(OnboardLogEntryLog, "AnalyzeView.QGCOnboardLogEntry") -MAVLinkLogDownloadData::MAVLinkLogDownloadData(QGCMAVLinkLogEntry * const logEntry) +OnboardLogDownloadData::OnboardLogDownloadData(QGCOnboardLogEntry * const logEntry) : ID(logEntry->id()) , entry(logEntry) { - // qCDebug(MAVLinkLogEntryLog) << Q_FUNC_INFO << this; + // qCDebug(OnboardLogEntryLog) << Q_FUNC_INFO << this; } -MAVLinkLogDownloadData::~MAVLinkLogDownloadData() +OnboardLogDownloadData::~OnboardLogDownloadData() { - // qCDebug(MAVLinkLogEntryLog) << Q_FUNC_INFO << this; + // qCDebug(OnboardLogEntryLog) << Q_FUNC_INFO << this; } -void MAVLinkLogDownloadData::advanceChunk() +void OnboardLogDownloadData::advanceChunk() { ++current_chunk; chunk_table = QBitArray(chunkBins(), false); } -uint32_t MAVLinkLogDownloadData::chunkBins() const +uint32_t OnboardLogDownloadData::chunkBins() const { const qreal num = static_cast((entry->size() - (current_chunk * kChunkSize))) / static_cast(MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); return qMin(static_cast(qCeil(num)), kTableBins); } -uint32_t MAVLinkLogDownloadData::numChunks() const +uint32_t OnboardLogDownloadData::numChunks() const { const qreal num = static_cast(entry->size()) / static_cast(kChunkSize); return qCeil(num); } -bool MAVLinkLogDownloadData::chunkEquals(const bool val) const +bool OnboardLogDownloadData::chunkEquals(const bool val) const { return (chunk_table == QBitArray(chunk_table.size(), val)); } /*===========================================================================*/ -QGCMAVLinkLogEntry::QGCMAVLinkLogEntry(uint logId, const QDateTime &dateTime, uint logSize, bool received, QObject *parent) +QGCOnboardLogEntry::QGCOnboardLogEntry(uint logId, const QDateTime &dateTime, uint logSize, bool received, QObject *parent) : QObject(parent) , _logID(logId) , _logSize(logSize) , _logTimeUTC(dateTime) , _received(received) { - // qCDebug(MAVLinkLogEntryLog) << Q_FUNC_INFO << this; + // qCDebug(OnboardLogEntryLog) << Q_FUNC_INFO << this; } -QGCMAVLinkLogEntry::~QGCMAVLinkLogEntry() +QGCOnboardLogEntry::~QGCOnboardLogEntry() { - // qCDebug(MAVLinkLogEntryLog) << Q_FUNC_INFO << this; + // qCDebug(OnboardLogEntryLog) << Q_FUNC_INFO << this; } -QString QGCMAVLinkLogEntry::sizeStr() const +QString QGCOnboardLogEntry::sizeStr() const { return qgcApp()->bigSizeToString(_logSize); } diff --git a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.h b/src/AnalyzeView/OnboardLogs/OnboardLogEntry.h similarity index 89% rename from src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.h rename to src/AnalyzeView/OnboardLogs/OnboardLogEntry.h index 61fa4549e194..567b26134654 100644 --- a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogEntry.h +++ b/src/AnalyzeView/OnboardLogs/OnboardLogEntry.h @@ -10,14 +10,14 @@ #include "MAVLinkLib.h" -class QGCMAVLinkLogEntry; +class QGCOnboardLogEntry; -Q_DECLARE_LOGGING_CATEGORY(MAVLinkLogEntryLog) +Q_DECLARE_LOGGING_CATEGORY(OnboardLogEntryLog) -struct MAVLinkLogDownloadData +struct OnboardLogDownloadData { - explicit MAVLinkLogDownloadData(QGCMAVLinkLogEntry * const logEntry); - ~MAVLinkLogDownloadData(); + explicit OnboardLogDownloadData(QGCOnboardLogEntry * const logEntry); + ~OnboardLogDownloadData(); void advanceChunk(); @@ -31,7 +31,7 @@ struct MAVLinkLogDownloadData bool chunkEquals(const bool val) const; uint ID = 0; - QGCMAVLinkLogEntry *const entry = nullptr; + QGCOnboardLogEntry *const entry = nullptr; QBitArray chunk_table; uint32_t current_chunk = 0; @@ -49,7 +49,7 @@ struct MAVLinkLogDownloadData /*===========================================================================*/ -class QGCMAVLinkLogEntry : public QObject +class QGCOnboardLogEntry : public QObject { Q_OBJECT // QML_ELEMENT @@ -63,8 +63,8 @@ class QGCMAVLinkLogEntry : public QObject Q_PROPERTY(QString status READ status NOTIFY statusChanged) public: - explicit QGCMAVLinkLogEntry(uint logId, const QDateTime &dateTime = QDateTime(), uint logSize = 0, bool received = false, QObject *parent = nullptr); - ~QGCMAVLinkLogEntry(); + explicit QGCOnboardLogEntry(uint logId, const QDateTime &dateTime = QDateTime(), uint logSize = 0, bool received = false, QObject *parent = nullptr); + ~QGCOnboardLogEntry(); uint id() const { return _logID; } uint size() const { return _logSize; } diff --git a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogIcon.svg b/src/AnalyzeView/OnboardLogs/OnboardLogIcon.svg similarity index 100% rename from src/AnalyzeView/MAVLinkLogs/MAVLinkLogIcon.svg rename to src/AnalyzeView/OnboardLogs/OnboardLogIcon.svg diff --git a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogPage.qml b/src/AnalyzeView/OnboardLogs/OnboardLogPage.qml similarity index 70% rename from src/AnalyzeView/MAVLinkLogs/MAVLinkLogPage.qml rename to src/AnalyzeView/OnboardLogs/OnboardLogPage.qml index 8c3736d3a6c9..7f253f0ff152 100644 --- a/src/AnalyzeView/MAVLinkLogs/MAVLinkLogPage.qml +++ b/src/AnalyzeView/OnboardLogs/OnboardLogPage.qml @@ -7,9 +7,9 @@ import QGroundControl import QGroundControl.Controls AnalyzePage { - id: mavlinkLogPage + id: onboardLogPage pageComponent: pageComponent - pageDescription: qsTr("MAVLink Log allows you to download binary log files from your vehicle. Click Refresh to get list of available logs.") + pageDescription: qsTr("Onboard Logs allows you to download binary log files from your vehicle. Click Refresh to get list of available logs.") Component { id: pageComponent @@ -18,7 +18,7 @@ AnalyzePage { width: availableWidth height: availableHeight - Component.onCompleted: MAVLinkLogController.refresh() + Component.onCompleted: OnboardLogController.refresh() QGCFlickable { Layout.fillWidth: true @@ -28,7 +28,7 @@ AnalyzePage { GridLayout { id: gridLayout - rows: MAVLinkLogController.model.count + 1 + rows: OnboardLogController.model.count + 1 columns: 5 flow: GridLayout.TopToBottom columnSpacing: ScreenTools.defaultFontPixelWidth @@ -40,7 +40,7 @@ AnalyzePage { } Repeater { - model: MAVLinkLogController.model + model: OnboardLogController.model QGCCheckBox { Binding on checkState { @@ -54,7 +54,7 @@ AnalyzePage { QGCLabel { text: qsTr("Id") } Repeater { - model: MAVLinkLogController.model + model: OnboardLogController.model QGCLabel { text: object.id } } @@ -62,7 +62,7 @@ AnalyzePage { QGCLabel { text: qsTr("Date") } Repeater { - model: MAVLinkLogController.model + model: OnboardLogController.model QGCLabel { text: { @@ -82,7 +82,7 @@ AnalyzePage { QGCLabel { text: qsTr("Size") } Repeater { - model: MAVLinkLogController.model + model: OnboardLogController.model QGCLabel { text: object.sizeStr } } @@ -90,7 +90,7 @@ AnalyzePage { QGCLabel { text: qsTr("Status") } Repeater { - model: MAVLinkLogController.model + model: OnboardLogController.model QGCLabel { text: object.status } } @@ -104,40 +104,40 @@ AnalyzePage { QGCButton { Layout.fillWidth: true - enabled: !MAVLinkLogController.requestingList && !MAVLinkLogController.downloadingLogs + enabled: !OnboardLogController.requestingList && !OnboardLogController.downloadingLogs text: qsTr("Refresh") onClicked: { if (!QGroundControl.multiVehicleManager.activeVehicle || QGroundControl.multiVehicleManager.activeVehicle.isOfflineEditingVehicle) { - QGroundControl.showMessageDialog(mavlinkLogPage, qsTr("MAVLink Log Refresh"), qsTr("You must be connected to a vehicle in order to download MAVLink logs.")) + QGroundControl.showMessageDialog(onboardLogPage, qsTr("Onboard Log Refresh"), qsTr("You must be connected to a vehicle in order to download onboard logs.")) return } - MAVLinkLogController.refresh() + OnboardLogController.refresh() } } QGCButton { Layout.fillWidth: true - enabled: !MAVLinkLogController.requestingList && !MAVLinkLogController.downloadingLogs + enabled: !OnboardLogController.requestingList && !OnboardLogController.downloadingLogs text: qsTr("Download") onClicked: { var logsSelected = false - for (var i = 0; i < MAVLinkLogController.model.count; i++) { - if (MAVLinkLogController.model.get(i).selected) { + for (var i = 0; i < OnboardLogController.model.count; i++) { + if (OnboardLogController.model.get(i).selected) { logsSelected = true break } } if (!logsSelected) { - QGroundControl.showMessageDialog(mavlinkLogPage, qsTr("MAVLink Log"), qsTr("You must select at least one MAVLink log file to download.")) + QGroundControl.showMessageDialog(onboardLogPage, qsTr("Onboard Log"), qsTr("You must select at least one onboard log file to download.")) return } if (ScreenTools.isMobile) { - MAVLinkLogController.download() + OnboardLogController.download() return } @@ -150,7 +150,7 @@ AnalyzePage { QGCFileDialog { id: fileDialog onAcceptedForLoad: (file) => { - MAVLinkLogController.download(file) + OnboardLogController.download(file) close() } } @@ -158,22 +158,22 @@ AnalyzePage { QGCButton { Layout.fillWidth: true - enabled: !MAVLinkLogController.requestingList && !MAVLinkLogController.downloadingLogs && (MAVLinkLogController.model.count > 0) + enabled: !OnboardLogController.requestingList && !OnboardLogController.downloadingLogs && (OnboardLogController.model.count > 0) text: qsTr("Erase All") onClicked: QGroundControl.showMessageDialog( - mavlinkLogPage, - qsTr("Delete All MAVLink Log Files"), - qsTr("All MAVLink log files will be erased permanently. Is this really what you want?"), + onboardLogPage, + qsTr("Delete All Onboard Log Files"), + qsTr("All onboard log files will be erased permanently. Is this really what you want?"), Dialog.Yes | Dialog.No, - function() { MAVLinkLogController.eraseAll() } + function() { OnboardLogController.eraseAll() } ) } QGCButton { Layout.fillWidth: true text: qsTr("Cancel") - enabled: MAVLinkLogController.requestingList || MAVLinkLogController.downloadingLogs - onClicked: MAVLinkLogController.cancel() + enabled: OnboardLogController.requestingList || OnboardLogController.downloadingLogs + onClicked: OnboardLogController.cancel() } } } diff --git a/test/AnalyzeView/CMakeLists.txt b/test/AnalyzeView/CMakeLists.txt index 9602acf24b36..99983281b002 100644 --- a/test/AnalyzeView/CMakeLists.txt +++ b/test/AnalyzeView/CMakeLists.txt @@ -13,8 +13,8 @@ target_sources(${CMAKE_PROJECT_NAME} MAVLinkConsoleControllerTest.h MAVLinkInspectorControllerTest.cc MAVLinkInspectorControllerTest.h - MAVLinkLogDownloadTest.cc - MAVLinkLogDownloadTest.h + OnboardLogDownloadTest.cc + OnboardLogDownloadTest.h MAVLinkMessageFieldTest.cc MAVLinkMessageFieldTest.h MAVLinkMessageTest.cc diff --git a/test/AnalyzeView/MAVLinkLogDownloadTest.cc b/test/AnalyzeView/OnboardLogDownloadTest.cc similarity index 83% rename from test/AnalyzeView/MAVLinkLogDownloadTest.cc rename to test/AnalyzeView/OnboardLogDownloadTest.cc index 6905a7e9b0b2..e77fe73ee3e6 100644 --- a/test/AnalyzeView/MAVLinkLogDownloadTest.cc +++ b/test/AnalyzeView/OnboardLogDownloadTest.cc @@ -1,18 +1,18 @@ -#include "MAVLinkLogDownloadTest.h" +#include "OnboardLogDownloadTest.h" #include -#include "MAVLinkLogController.h" -#include "MAVLinkLogEntry.h" +#include "OnboardLogController.h" +#include "OnboardLogEntry.h" #include "MAVLinkProtocol.h" #include "MultiSignalSpy.h" #include "MultiVehicleManager.h" #include "QmlObjectListModel.h" -void MAVLinkLogDownloadTest::_downloadTest() +void OnboardLogDownloadTest::_downloadTest() { // VehicleTest::init() already connects the mock link - MAVLinkLogController* const controller = new MAVLinkLogController(this); + OnboardLogController* const controller = new OnboardLogController(this); MultiSignalSpy* multiSpyLogDownloadController = new MultiSignalSpy(this); QVERIFY(multiSpyLogDownloadController->init(controller)); controller->refresh(); @@ -25,7 +25,7 @@ void MAVLinkLogDownloadTest::_downloadTest() multiSpyLogDownloadController->clearAllSignals(); QmlObjectListModel* const model = controller->_getModel(); QVERIFY(model); - model->value(0)->setSelected(true); + model->value(0)->setSelected(true); const QString downloadTo = QDir::currentPath(); controller->download(downloadTo); QVERIFY(multiSpyLogDownloadController->waitForSignal("downloadingLogsChanged", TestTimeout::longMs())); @@ -40,4 +40,4 @@ void MAVLinkLogDownloadTest::_downloadTest() (void)QFile::remove(downloadFile); } -UT_REGISTER_TEST(MAVLinkLogDownloadTest, TestLabel::Integration, TestLabel::AnalyzeView, TestLabel::Vehicle) +UT_REGISTER_TEST(OnboardLogDownloadTest, TestLabel::Integration, TestLabel::AnalyzeView, TestLabel::Vehicle) diff --git a/test/AnalyzeView/MAVLinkLogDownloadTest.h b/test/AnalyzeView/OnboardLogDownloadTest.h similarity index 69% rename from test/AnalyzeView/MAVLinkLogDownloadTest.h rename to test/AnalyzeView/OnboardLogDownloadTest.h index 4b8e48e5a11e..b0cf94849f6a 100644 --- a/test/AnalyzeView/MAVLinkLogDownloadTest.h +++ b/test/AnalyzeView/OnboardLogDownloadTest.h @@ -2,7 +2,7 @@ #include "BaseClasses/VehicleTest.h" -class MAVLinkLogDownloadTest : public VehicleTest +class OnboardLogDownloadTest : public VehicleTest { Q_OBJECT diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d492c7460a30..952b9fb4a8d3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -76,7 +76,7 @@ add_qgc_test(ULogParserTest LABELS Unit AnalyzeView RESOURCE_LOCK TempFiles) add_qgc_test(MAVLinkChartControllerTest LABELS Unit AnalyzeView) add_qgc_test(MAVLinkConsoleControllerTest LABELS Unit AnalyzeView) add_qgc_test(MAVLinkInspectorControllerTest LABELS Unit AnalyzeView) -add_qgc_test(MAVLinkLogDownloadTest LABELS Integration AnalyzeView Vehicle RESOURCE_LOCK MockLink) +add_qgc_test(OnboardLogDownloadTest LABELS Integration AnalyzeView Vehicle RESOURCE_LOCK MockLink) add_qgc_test(MAVLinkMessageFieldTest LABELS Unit AnalyzeView) add_qgc_test(MAVLinkMessageTest LABELS Unit AnalyzeView) add_qgc_test(MAVLinkSystemTest LABELS Unit AnalyzeView) From 53f47f9efede0957d5d6ecce8f81879dd84827c0 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 15 Mar 2026 16:20:34 -0700 Subject: [PATCH 107/119] Support ArduPilot 4.7 parameter renames ArduPilot 4.7 renames many parameters to new names with SI units. This adds remap tables so QGC can work with both old and new firmware. Key changes: - Add 4.7 remap tables for Copter, Plane, Rover, and Sub - Replace r. prefix system with noremap. bypass and always-on remap - Use noremap. for version detection where old/new params have different units (speed, altitude, RTL defaults) - Update all QML files to use new 4.7 parameter names - Rename maximumHorizontalSpeedMultirotor to maximumHorizontalSpeedMultirotorMetersSecond for clarity - Fix null dereference in APMFollowComponent.qml - Handle removed PSC_ACCZ_FILT parameter gracefully Fixes #14079 --- .../APM/APMFlightModesComponent.qml | 2 +- .../APM/APMFollowComponent.qml | 2 +- .../APM/APMPowerComponent.qml | 8 +- .../APM/APMSafetyComponent.qml | 19 ++-- .../APM/APMSafetyComponentSub.qml | 8 +- .../APM/APMSafetyComponentSummary.qml | 6 +- .../APM/APMSafetyComponentSummarySub.qml | 6 +- .../APM/APMTuningComponentCopter.qml | 26 +++--- .../APM/APMTuningComponentSub.qml | 63 ++++++------- src/FactSystem/ParameterManager.cc | 12 +-- src/FactSystem/ParameterManager.h | 9 +- src/FirmwarePlugin/APM/APMFirmwarePlugin.cc | 55 +++++++++--- src/FirmwarePlugin/APM/APMFirmwarePlugin.h | 2 +- .../APM/APMFlightModeIndicator.qml | 7 +- .../APM/ArduCopterFirmwarePlugin.cc | 88 ++++++++++++++++++- .../APM/ArduPlaneFirmwarePlugin.cc | 67 +++++++++++++- .../APM/ArduRoverFirmwarePlugin.cc | 14 ++- .../APM/ArduSubFirmwarePlugin.cc | 66 +++++++++++++- src/FirmwarePlugin/FirmwarePlugin.h | 34 +++++-- src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc | 4 +- src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h | 2 +- src/FlyView/GuidedActionsController.qml | 4 +- src/Vehicle/Vehicle.cc | 4 +- src/Vehicle/Vehicle.h | 2 +- 24 files changed, 393 insertions(+), 117 deletions(-) diff --git a/src/AutoPilotPlugins/APM/APMFlightModesComponent.qml b/src/AutoPilotPlugins/APM/APMFlightModesComponent.qml index dcb250d4cf70..cfcdf8cf7d61 100644 --- a/src/AutoPilotPlugins/APM/APMFlightModesComponent.qml +++ b/src/AutoPilotPlugins/APM/APMFlightModesComponent.qml @@ -204,7 +204,7 @@ SetupPage { FactComboBox { id: optCombo width: ScreenTools.defaultFontPixelWidth * 15 - fact: controller.getParameterFact(-1, "r.RC" + index + "_OPTION") + fact: controller.getParameterFact(-1, "RC" + index + "_OPTION") indexModel: false } } diff --git a/src/AutoPilotPlugins/APM/APMFollowComponent.qml b/src/AutoPilotPlugins/APM/APMFollowComponent.qml index 24b8a262a164..ef1c53eaf556 100644 --- a/src/AutoPilotPlugins/APM/APMFollowComponent.qml +++ b/src/AutoPilotPlugins/APM/APMFollowComponent.qml @@ -368,7 +368,7 @@ SetupPage { transform: Rotation { origin.x: vehicleIcon.width / 2 origin.y: vehicleIcon.height / 2 - angle: _roverFirmware ? 0 : + angle: _roverFirmware || !_followYawBehavior ? 0 : (_followYawBehavior.rawValue == _followYawBehaviorNone ? 0 : (_followYawBehavior.rawValue == _followYawBehaviorFace ? diff --git a/src/AutoPilotPlugins/APM/APMPowerComponent.qml b/src/AutoPilotPlugins/APM/APMPowerComponent.qml index 8452c5e5bce3..a989498aeeaf 100644 --- a/src/AutoPilotPlugins/APM/APMPowerComponent.qml +++ b/src/AutoPilotPlugins/APM/APMPowerComponent.qml @@ -111,8 +111,8 @@ SetupPage { anchors.left: parent.left sourceComponent: _batt1FullSettings.visible ? powerSetupComponent : undefined - property Fact armVoltMin: controller.getParameterFact(-1, "r.BATT_ARM_VOLT", false /* reportMissing */) - property Fact battAmpPerVolt: controller.getParameterFact(-1, "r.BATT_AMP_PERVLT", false /* reportMissing */) + property Fact armVoltMin: controller.getParameterFact(-1, "BATT_ARM_VOLT", false /* reportMissing */) + property Fact battAmpPerVolt: controller.getParameterFact(-1, "BATT_AMP_PERVLT", false /* reportMissing */) property Fact battAmpOffset: controller.getParameterFact(-1, "BATT_AMP_OFFSET", false /* reportMissing */) property Fact battCapacity: controller.getParameterFact(-1, "BATT_CAPACITY", false /* reportMissing */) property Fact battCurrPin: controller.getParameterFact(-1, "BATT_CURR_PIN", false /* reportMissing */) @@ -198,8 +198,8 @@ SetupPage { anchors.left: parent.left sourceComponent: batt2FullSettings.visible ? powerSetupComponent : undefined - property Fact armVoltMin: controller.getParameterFact(-1, "r.BATT2_ARM_VOLT", false /* reportMissing */) - property Fact battAmpPerVolt: controller.getParameterFact(-1, "r.BATT2_AMP_PERVLT", false /* reportMissing */) + property Fact armVoltMin: controller.getParameterFact(-1, "BATT2_ARM_VOLT", false /* reportMissing */) + property Fact battAmpPerVolt: controller.getParameterFact(-1, "BATT2_AMP_PERVLT", false /* reportMissing */) property Fact battAmpOffset: controller.getParameterFact(-1, "BATT2_AMP_OFFSET", false /* reportMissing */) property Fact battCapacity: controller.getParameterFact(-1, "BATT2_CAPACITY", false /* reportMissing */) property Fact battCurrPin: controller.getParameterFact(-1, "BATT2_CURR_PIN", false /* reportMissing */) diff --git a/src/AutoPilotPlugins/APM/APMSafetyComponent.qml b/src/AutoPilotPlugins/APM/APMSafetyComponent.qml index 29ac36b6b2b9..cf65e704a688 100644 --- a/src/AutoPilotPlugins/APM/APMSafetyComponent.qml +++ b/src/AutoPilotPlugins/APM/APMSafetyComponent.qml @@ -325,9 +325,9 @@ SetupPage { spacing: _margins / 2 property Fact _failsafeGCSEnable: controller.getParameterFact(-1, "FS_GCS_ENABLE") - property Fact _failsafeBattLowAct: controller.getParameterFact(-1, "r.BATT_FS_LOW_ACT", false /* reportMissing */) - property Fact _failsafeBattMah: controller.getParameterFact(-1, "r.BATT_LOW_MAH", false /* reportMissing */) - property Fact _failsafeBattVoltage: controller.getParameterFact(-1, "r.BATT_LOW_VOLT", false /* reportMissing */) + property Fact _failsafeBattLowAct: controller.getParameterFact(-1, "BATT_FS_LOW_ACT", false /* reportMissing */) + property Fact _failsafeBattMah: controller.getParameterFact(-1, "BATT_LOW_MAH", false /* reportMissing */) + property Fact _failsafeBattVoltage: controller.getParameterFact(-1, "BATT_LOW_VOLT", false /* reportMissing */) property Fact _failsafeThrEnable: controller.getParameterFact(-1, "FS_THR_ENABLE") property Fact _failsafeThrValue: controller.getParameterFact(-1, "FS_THR_VALUE") @@ -524,10 +524,12 @@ SetupPage { Column { spacing: _margins / 2 - property Fact _landSpeedFact: controller.getParameterFact(-1, "LAND_SPEED") - property Fact _rtlAltFact: controller.getParameterFact(-1, "RTL_ALT") + property Fact _landSpeedFact: controller.getParameterFact(-1, "LAND_SPD_MS") + property Fact _rtlAltFact: controller.getParameterFact(-1, "RTL_ALT_M") property Fact _rtlLoitTimeFact: controller.getParameterFact(-1, "RTL_LOIT_TIME") - property Fact _rtlAltFinalFact: controller.getParameterFact(-1, "RTL_ALT_FINAL") + property Fact _rtlAltFinalFact: controller.getParameterFact(-1, "RTL_ALT_FINAL_M") + // RTL_ALT_M (4.7+) is in meters, RTL_ALT (pre-4.7) is in centimeters + property bool _rtlAltIsMeters: controller.parameterExists(-1, "noremap.RTL_ALT_M") QGCLabel { id: rtlLabel @@ -575,7 +577,8 @@ SetupPage { text: qsTr("Return at specified altitude:") checked: _rtlAltFact.value != 0 - onClicked: _rtlAltFact.value = 1500 + // RTL_ALT_M (4.7+) is in meters, RTL_ALT (pre-4.7) is in centimeters + onClicked: _rtlAltFact.value = _rtlAltIsMeters ? 15 : 1500 } FactTextField { @@ -651,7 +654,7 @@ SetupPage { Column { spacing: _margins / 2 - property Fact _rtlAltFact: controller.getParameterFact(-1, "r.RTL_ALTITUDE") + property Fact _rtlAltFact: controller.getParameterFact(-1, "RTL_ALTITUDE") QGCLabel { text: qsTr("Return to Launch") diff --git a/src/AutoPilotPlugins/APM/APMSafetyComponentSub.qml b/src/AutoPilotPlugins/APM/APMSafetyComponentSub.qml index d43c1f97b604..259373685437 100644 --- a/src/AutoPilotPlugins/APM/APMSafetyComponentSub.qml +++ b/src/AutoPilotPlugins/APM/APMSafetyComponentSub.qml @@ -24,7 +24,7 @@ SetupPage { property bool _firmware34: globals.activeVehicle.versionCompare(3, 5, 0) < 0 // Enable/Action parameters - property Fact _failsafeBatteryEnable: controller.getParameterFact(-1, "r.BATT_FS_LOW_ACT", false) + property Fact _failsafeBatteryEnable: controller.getParameterFact(-1, "BATT_FS_LOW_ACT", false) property Fact _failsafeEKFEnable: controller.getParameterFact(-1, "FS_EKF_ACTION") property Fact _failsafeGCSEnable: controller.getParameterFact(-1, "FS_GCS_ENABLE") property Fact _failsafeLeakEnable: controller.getParameterFact(-1, "FS_LEAK_ENABLE") @@ -39,9 +39,9 @@ SetupPage { property Fact _failsafeLeakPin: controller.getParameterFact(-1, "LEAK1_PIN") property Fact _failsafeLeakLogic: controller.getParameterFact(-1, "LEAK1_LOGIC") property Fact _failsafeEKFThreshold: controller.getParameterFact(-1, "FS_EKF_THRESH") - property Fact _failsafeBatteryVoltage: controller.getParameterFact(-1, "r.BATT_LOW_VOLT", false) - property Fact _failsafeBatteryCapacity: controller.getParameterFact(-1, "r.BATT_LOW_MAH", false) - property bool _batteryDetected: controller.parameterExists(-1, "r.BATT_LOW_MAH") + property Fact _failsafeBatteryVoltage: controller.getParameterFact(-1, "BATT_LOW_VOLT", false) + property Fact _failsafeBatteryCapacity: controller.getParameterFact(-1, "BATT_LOW_MAH", false) + property bool _batteryDetected: controller.parameterExists(-1, "BATT_LOW_MAH") // Older firmwares use ARMING_CHECK. Newer firmwares use ARMING_SKIPCHK. property Fact _armingCheck: controller.getParameterFact(-1, "ARMING_CHECK", false /* reportMissing */) diff --git a/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml b/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml index 4d6ba962227d..3c3583a83696 100644 --- a/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml @@ -23,7 +23,7 @@ Item { property bool _batt1MonitorEnabled: _batt1Monitor.rawValue !== 0 property bool _batt2MonitorEnabled: _batt2MonitorAvailable && _batt2Monitor.rawValue !== 0 - property Fact _batt1FSLowAct: controller.getParameterFact(-1, "r.BATT_FS_LOW_ACT", false /* reportMissing */) + property Fact _batt1FSLowAct: controller.getParameterFact(-1, "BATT_FS_LOW_ACT", false /* reportMissing */) property Fact _batt1FSCritAct: controller.getParameterFact(-1, "BATT_FS_CRT_ACT", false /* reportMissing */) property Fact _batt2FSLowAct: controller.getParameterFact(-1, "BATT2_FS_LOW_ACT", false /* reportMissing */) property Fact _batt2FSCritAct: controller.getParameterFact(-1, "BATT2_FS_CRT_ACT", false /* reportMissing */) @@ -150,7 +150,7 @@ Item { valueText: fact ? (fact.value == 0 ? qsTr("current") : fact.valueString + " " + fact.units) : "" visible: controller.vehicle.multiRotor - property Fact fact: controller.getParameterFact(-1, "RTL_ALT", false /* reportMissing */) + property Fact fact: controller.getParameterFact(-1, "RTL_ALT_M", false /* reportMissing */) } VehicleSummaryRow { @@ -158,7 +158,7 @@ Item { valueText: fact ? (fact.value < 0 ? qsTr("current") : fact.valueString + " " + fact.units) : "" visible: controller.vehicle.fixedWing - property Fact fact: controller.getParameterFact(-1, "r.RTL_ALTITUDE", false /* reportMissing */) + property Fact fact: controller.getParameterFact(-1, "RTL_ALTITUDE", false /* reportMissing */) } } } diff --git a/src/AutoPilotPlugins/APM/APMSafetyComponentSummarySub.qml b/src/AutoPilotPlugins/APM/APMSafetyComponentSummarySub.qml index 9e914cda8409..2bd81dc5646e 100644 --- a/src/AutoPilotPlugins/APM/APMSafetyComponentSummarySub.qml +++ b/src/AutoPilotPlugins/APM/APMSafetyComponentSummarySub.qml @@ -13,7 +13,7 @@ Item { FactPanelController { id: controller; } // Enable/Action parameters - property Fact _failsafeBatteryEnable: controller.getParameterFact(-1, "r.BATT_FS_LOW_ACT", false) + property Fact _failsafeBatteryEnable: controller.getParameterFact(-1, "BATT_FS_LOW_ACT", false) property Fact _failsafeEKFEnable: controller.getParameterFact(-1, "FS_EKF_ACTION") property Fact _failsafeGCSEnable: controller.getParameterFact(-1, "FS_GCS_ENABLE") property Fact _failsafeLeakEnable: controller.getParameterFact(-1, "FS_LEAK_ENABLE") @@ -28,8 +28,8 @@ Item { property Fact _failsafeLeakPin: controller.getParameterFact(-1, "LEAK1_PIN") property Fact _failsafeLeakLogic: controller.getParameterFact(-1, "LEAK1_LOGIC") property Fact _failsafeEKFThreshold: controller.getParameterFact(-1, "FS_EKF_THRESH") - property Fact _failsafeBatteryVoltage: controller.getParameterFact(-1, "r.BATT_LOW_VOLT", false) - property Fact _failsafeBatteryCapacity: controller.getParameterFact(-1, "r.BATT_LOW_MAH", false) + property Fact _failsafeBatteryVoltage: controller.getParameterFact(-1, "BATT_LOW_VOLT", false) + property Fact _failsafeBatteryCapacity: controller.getParameterFact(-1, "BATT_LOW_MAH", false) // Older firmwares use ARMING_CHECK. Newer firmwares use ARMING_SKIPCHK. property Fact _armingCheck: controller.getParameterFact(-1, "ARMING_CHECK", false /* reportMissing */) diff --git a/src/AutoPilotPlugins/APM/APMTuningComponentCopter.qml b/src/AutoPilotPlugins/APM/APMTuningComponentCopter.qml index cdcd618815c5..64f884d775a1 100644 --- a/src/AutoPilotPlugins/APM/APMTuningComponentCopter.qml +++ b/src/AutoPilotPlugins/APM/APMTuningComponentCopter.qml @@ -24,17 +24,17 @@ SetupPage { property Fact _rateRollI: controller.getParameterFact(-1, "ATC_RAT_RLL_I") property Fact _ratePitchP: controller.getParameterFact(-1, "ATC_RAT_PIT_P") property Fact _ratePitchI: controller.getParameterFact(-1, "ATC_RAT_PIT_I") - property Fact _rateClimbP: controller.getParameterFact(-1, "PSC_ACCZ_P") - property Fact _rateClimbI: controller.getParameterFact(-1, "PSC_ACCZ_I") + property Fact _rateClimbP: controller.getParameterFact(-1, "PSC_D_ACC_P") + property Fact _rateClimbI: controller.getParameterFact(-1, "PSC_D_ACC_I") property Fact _motSpinArm: controller.getParameterFact(-1, "MOT_SPIN_ARM") property Fact _motSpinMin: controller.getParameterFact(-1, "MOT_SPIN_MIN") - property Fact _ch7Opt: controller.getParameterFact(-1, "r.RC7_OPTION") - property Fact _ch8Opt: controller.getParameterFact(-1, "r.RC8_OPTION") - property Fact _ch9Opt: controller.getParameterFact(-1, "r.RC9_OPTION") - property Fact _ch10Opt: controller.getParameterFact(-1, "r.RC10_OPTION") - property Fact _ch11Opt: controller.getParameterFact(-1, "r.RC11_OPTION") - property Fact _ch12Opt: controller.getParameterFact(-1, "r.RC12_OPTION") + property Fact _ch7Opt: controller.getParameterFact(-1, "RC7_OPTION") + property Fact _ch8Opt: controller.getParameterFact(-1, "RC8_OPTION") + property Fact _ch9Opt: controller.getParameterFact(-1, "RC9_OPTION") + property Fact _ch10Opt: controller.getParameterFact(-1, "RC10_OPTION") + property Fact _ch11Opt: controller.getParameterFact(-1, "RC11_OPTION") + property Fact _ch12Opt: controller.getParameterFact(-1, "RC12_OPTION") readonly property int _firstOptionChannel: 7 readonly property int _lastOptionChannel: 12 @@ -59,7 +59,7 @@ SetupPage { function calcAutoTuneChannel() { _autoTuneSwitchChannelIndex = 0 for (var channel=_firstOptionChannel; channel<=_lastOptionChannel; channel++) { - var optionFact = controller.getParameterFact(-1, "r.RC" + channel + "_OPTION") + var optionFact = controller.getParameterFact(-1, "RC" + channel + "_OPTION") if (optionFact.value == _autoTuneOption) { _autoTuneSwitchChannelIndex = channel - _firstOptionChannel + 1 break @@ -71,7 +71,7 @@ SetupPage { function setChannelAutoTuneOption(channel) { // First clear any previous settings for AutTune for (var optionChannel=_firstOptionChannel; optionChannel<=_lastOptionChannel; optionChannel++) { - var optionFact = controller.getParameterFact(-1, "r.RC" + optionChannel + "_OPTION") + var optionFact = controller.getParameterFact(-1, "RC" + optionChannel + "_OPTION") if (optionFact.value == _autoTuneOption) { optionFact.value = 0 } @@ -79,7 +79,7 @@ SetupPage { // Now set the function into the new channel if (channel != 0) { - var optionFact = controller.getParameterFact(-1, "r.RC" + channel + "_OPTION") + var optionFact = controller.getParameterFact(-1, "RC" + channel + "_OPTION") optionFact.value = _autoTuneOption } } @@ -239,14 +239,14 @@ SetupPage { id: tuneMinField textField.validator: DoubleValidator {bottom: 0; top: 32767;} label: qsTr("Min:") - fact: controller.getParameterFact(-1, "r.TUNE_MIN") + fact: controller.getParameterFact(-1, "TUNE_MIN") } LabelledFactTextField { id: tuneMaxField textField.validator: DoubleValidator {bottom: 0; top: 32767;} label: qsTr("Max:") - fact: controller.getParameterFact(-1, "r.TUNE_MAX") + fact: controller.getParameterFact(-1, "TUNE_MAX") } } } diff --git a/src/AutoPilotPlugins/APM/APMTuningComponentSub.qml b/src/AutoPilotPlugins/APM/APMTuningComponentSub.qml index 2ba07fa15a6e..6e359012579a 100644 --- a/src/AutoPilotPlugins/APM/APMTuningComponentSub.qml +++ b/src/AutoPilotPlugins/APM/APMTuningComponentSub.qml @@ -100,17 +100,20 @@ SetupPage { anchors.top: parent.top spacing: _margins*1.5 - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_POSXY_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_POSZ_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_I") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_IMAX") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELZ_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_D") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_FILT") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_I") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_IMAX") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_POSXY_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_POSZ_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_VELXY_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_VELXY_I") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_VELXY_IMAX") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_VELZ_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_ACCZ_D") } + FactTextFieldSlider2 { + visible: controller.parameterExists(-1, "PSC_ACCZ_FILT") + fact: visible ? controller.getParameterFact(-1, "PSC_ACCZ_FILT") : null + } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_ACCZ_I") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_ACCZ_IMAX") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_ACCZ_P") } } // Column - VEL parameters } @@ -123,19 +126,19 @@ SetupPage { anchors.top: parent.top spacing: _margins*1.5 - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_POSXY_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_POSZ_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_I") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_IMAX") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELZ_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_D") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_FLTD") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_FLTE") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_FLTT") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_I") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_IMAX") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_NE_POS_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_POS_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_NE_VEL_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_NE_VEL_I") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_NE_VEL_IMAX") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_VEL_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_D") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_FLTD") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_FLTE") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_FLTT") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_I") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_IMAX") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_P") } } // Column - VEL parameters } @@ -191,12 +194,12 @@ SetupPage { anchors.top: parent.top spacing: _margins*1.5 - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_ACCEL") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_ACCEL_Z") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_RADIUS") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_SPEED") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_SPEED_DN") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_SPEED_UP") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_ACC") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_ACC_Z") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_RADIUS_M") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_SPD") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_SPD_DN") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_SPD_UP") } FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "LOIT_SPEED") } FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "LOIT_ACC_MAX") } FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "LOIT_ANG_MAX") } diff --git a/src/FactSystem/ParameterManager.cc b/src/FactSystem/ParameterManager.cc index d0d4333616a1..46a2d4e57969 100644 --- a/src/FactSystem/ParameterManager.cc +++ b/src/FactSystem/ParameterManager.cc @@ -1283,9 +1283,9 @@ void ParameterManager::_paramRequestListTimeout() QString ParameterManager::_remapParamNameToVersion(const QString ¶mName) const { - if (!paramName.startsWith(QStringLiteral("r."))) { - // No version mapping wanted - return paramName; + static const QString noRemapPrefix = QStringLiteral("noremap."); + if (paramName.startsWith(noRemapPrefix)) { + return paramName.mid(noRemapPrefix.length()); } const int majorVersion = _vehicle->firmwareMajorVersion(); @@ -1293,21 +1293,21 @@ QString ParameterManager::_remapParamNameToVersion(const QString ¶mName) con qCDebug(ParameterManagerLog) << "_remapParamNameToVersion" << paramName << majorVersion << minorVersion; - QString mappedParamName = paramName.right(paramName.length() - 2); if (majorVersion == Vehicle::versionNotSetValue) { // Vehicle version unknown - return mappedParamName; + return paramName; } const FirmwarePlugin::remapParamNameMajorVersionMap_t &majorVersionRemap = _vehicle->firmwarePlugin()->paramNameRemapMajorVersionMap(); if (!majorVersionRemap.contains(majorVersion)) { // No mapping for this major version qCDebug(ParameterManagerLog) << "_remapParamNameToVersion: no major version mapping"; - return mappedParamName; + return paramName; } const FirmwarePlugin::remapParamNameMinorVersionRemapMap_t &remapMinorVersion = majorVersionRemap[majorVersion]; // We must map backwards from the highest known minor version to one above the vehicle's minor version + QString mappedParamName = paramName; for (int currentMinorVersion = _vehicle->firmwarePlugin()->remapParamNameHigestMinorVersionNumber(majorVersion); currentMinorVersion>minorVersion; currentMinorVersion--) { if (remapMinorVersion.contains(currentMinorVersion)) { const FirmwarePlugin::remapParamNameMap_t &remap = remapMinorVersion[currentMinorVersion]; diff --git a/src/FactSystem/ParameterManager.h b/src/FactSystem/ParameterManager.h index ff47303a4d08..1fef68d92096 100644 --- a/src/FactSystem/ParameterManager.h +++ b/src/FactSystem/ParameterManager.h @@ -133,7 +133,14 @@ private slots: void _tryCacheHashLoad(int vehicleId, int componentId, const QVariant &hashValue); void _loadMetaData(); void _clearMetaData(); - /// Remap a parameter from one firmware version to another + /// Remap a parameter name from the newest firmware version to the version running on the vehicle. + /// All parameter names are walked backwards through the FirmwarePlugin remap tables from the + /// highest known minor version down to the vehicle's actual version. Names not found in any + /// remap table pass through unchanged. + /// + /// Names prefixed with "noremap." bypass remapping entirely — the prefix is stripped and the + /// bare name is used as-is. This is needed when code must distinguish old vs new parameter + /// names for unit conversion (e.g. checking whether WPNAV_SPEED exists vs WP_SPD). QString _remapParamNameToVersion(const QString ¶mName) const; bool _fillMavlinkParamUnion(FactMetaData::ValueType_t valueType, const QVariant &rawValue, mavlink_param_union_t ¶mUnion) const; bool _mavlinkParamUnionToVariant(const mavlink_param_union_t ¶mUnion, QVariant &outValue) const; diff --git a/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc b/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc index 84e6ed1696c3..0458b3bb6c1a 100644 --- a/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc @@ -912,18 +912,28 @@ void APMFirmwarePlugin::guidedModeChangeAltitude(Vehicle *vehicle, double altitu bool APMFirmwarePlugin::mulirotorSpeedLimitsAvailable(Vehicle *vehicle) const { - return vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "WPNAV_SPEED"); + // Use noremap. to bypass remap and check for specific parameter names directly, + // since the old and new parameters have different units. + return vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.WP_SPD")) + || vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.WPNAV_SPEED")); } -double APMFirmwarePlugin::maximumHorizontalSpeedMultirotor(Vehicle *vehicle) const +double APMFirmwarePlugin::maximumHorizontalSpeedMultirotorMetersSecond(Vehicle *vehicle) const { - const QString speedParam("WPNAV_SPEED"); + // Use noremap. to bypass remap and check for specific parameter names directly, + // since the old and new parameters have different units. - if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, speedParam)) { - return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, speedParam)->rawValue().toDouble() * 0.01; // note cm/s -> m/s + // 4.7+: WP_SPD is in m/s + if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.WP_SPD"))) { + return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.WP_SPD"))->rawValue().toDouble(); } - return FirmwarePlugin::maximumHorizontalSpeedMultirotor(vehicle); + // pre-4.7: WPNAV_SPEED is in cm/s + if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.WPNAV_SPEED"))) { + return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.WPNAV_SPEED"))->rawValue().toDouble() * 0.01; + } + + return FirmwarePlugin::maximumHorizontalSpeedMultirotorMetersSecond(vehicle); } void APMFirmwarePlugin::guidedModeChangeGroundSpeedMetersSecond(Vehicle *vehicle, double groundspeed) const @@ -985,11 +995,28 @@ void APMFirmwarePlugin::guidedModeChangeHeading(Vehicle *vehicle, const QGeoCoor double APMFirmwarePlugin::minimumTakeoffAltitudeMeters(Vehicle* vehicle) const { double minTakeoffAlt = 0; - const QString takeoffAltParam(vehicle->vtol() ? QStringLiteral("Q_RTL_ALT") : QStringLiteral("PILOT_TKOFF_ALT")); - const float paramDivisor = vehicle->vtol() ? 1.0 : 100.0; // PILOT_TAKEOFF_ALT is in centimeters - if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, takeoffAltParam)) { - minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, takeoffAltParam)->rawValue().toDouble() / static_cast(paramDivisor); + // Use noremap. to bypass remap and check for specific parameter names directly, + // since the old and new parameters have different units. + + if (vehicle->vtol()) { + // 4.7+: Q_PILOT_TKO_ALT_M (meters) + if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.Q_PILOT_TKO_ALT_M"))) { + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.Q_PILOT_TKO_ALT_M"))->rawValue().toDouble(); + } else if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.Q_PILOT_TKOFF_ALT"))) { + // pre-4.7: Q_PILOT_TKOFF_ALT (centimeters) + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.Q_PILOT_TKOFF_ALT"))->rawValue().toDouble() / 100.0; + } else if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("Q_RTL_ALT"))) { + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("Q_RTL_ALT"))->rawValue().toDouble(); + } + } else { + // 4.7+: PILOT_TKO_ALT_M (meters) + if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.PILOT_TKO_ALT_M"))) { + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.PILOT_TKO_ALT_M"))->rawValue().toDouble(); + } else if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.PILOT_TKOFF_ALT"))) { + // pre-4.7: PILOT_TKOFF_ALT (centimeters) + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.PILOT_TKOFF_ALT"))->rawValue().toDouble() / 100.0; + } } if (minTakeoffAlt == 0) { @@ -1232,7 +1259,7 @@ QMutex &APMFirmwarePlugin::_reencodeMavlinkChannelMutex() double APMFirmwarePlugin::maximumEquivalentAirspeed(Vehicle *vehicle) const { - const QString airspeedMax("r.AIRSPEED_MAX"); + const QString airspeedMax("AIRSPEED_MAX"); if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, airspeedMax)) { return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, airspeedMax)->rawValue().toDouble(); @@ -1243,7 +1270,7 @@ double APMFirmwarePlugin::maximumEquivalentAirspeed(Vehicle *vehicle) const double APMFirmwarePlugin::minimumEquivalentAirspeed(Vehicle *vehicle) const { - const QString airspeedMin("r.AIRSPEED_MIN"); + const QString airspeedMin("AIRSPEED_MIN"); if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, airspeedMin)) { return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, airspeedMin)->rawValue().toDouble(); @@ -1254,8 +1281,8 @@ double APMFirmwarePlugin::minimumEquivalentAirspeed(Vehicle *vehicle) const bool APMFirmwarePlugin::fixedWingAirSpeedLimitsAvailable(Vehicle *vehicle) const { - return vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "r.AIRSPEED_MIN") && - vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "r.AIRSPEED_MAX"); + return vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "AIRSPEED_MIN") && + vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "AIRSPEED_MAX"); } void APMFirmwarePlugin::guidedModeChangeEquivalentAirspeedMetersSecond(Vehicle *vehicle, double airspeed_equiv) const diff --git a/src/FirmwarePlugin/APM/APMFirmwarePlugin.h b/src/FirmwarePlugin/APM/APMFirmwarePlugin.h index 7c786f303080..fc0dab63600b 100644 --- a/src/FirmwarePlugin/APM/APMFirmwarePlugin.h +++ b/src/FirmwarePlugin/APM/APMFirmwarePlugin.h @@ -73,7 +73,7 @@ class APMFirmwarePlugin : public FirmwarePlugin // support for changing speed in Copter guide mode: bool mulirotorSpeedLimitsAvailable(Vehicle *vehicle) const override; - double maximumHorizontalSpeedMultirotor(Vehicle *vehicle) const override; + double maximumHorizontalSpeedMultirotorMetersSecond(Vehicle *vehicle) const override; void guidedModeChangeGroundSpeedMetersSecond(Vehicle *vehicle, double speed) const override; static QPair startCompensatingBaro(Vehicle *vehicle); diff --git a/src/FirmwarePlugin/APM/APMFlightModeIndicator.qml b/src/FirmwarePlugin/APM/APMFlightModeIndicator.qml index 3e1087e5ab5f..b016864d806b 100644 --- a/src/FirmwarePlugin/APM/APMFlightModeIndicator.qml +++ b/src/FirmwarePlugin/APM/APMFlightModeIndicator.qml @@ -12,7 +12,9 @@ SettingsGroupLayout { visible: activeVehicle.multiRotor property var activeVehicle: QGroundControl.multiVehicleManager.activeVehicle - property Fact rtlAltFact: controller.getParameterFact(-1, "RTL_ALT") + property Fact rtlAltFact: controller.getParameterFact(-1, "RTL_ALT_M") + // RTL_ALT_M (4.7+) is in meters, RTL_ALT (pre-4.7) is in centimeters + property bool _rtlAltIsMeters: controller.parameterExists(-1, "noremap.RTL_ALT_M") FactPanelController { id: controller } @@ -45,7 +47,8 @@ SettingsGroupLayout { if (index === 0) { rtlAltFact.rawValue = 0 } else { - rtlAltFact.rawValue = 1500 + // RTL_ALT_M (4.7+) is in meters, RTL_ALT (pre-4.7) is in centimeters + rtlAltFact.rawValue = _rtlAltIsMeters ? 15 : 1500 } } diff --git a/src/FirmwarePlugin/APM/ArduCopterFirmwarePlugin.cc b/src/FirmwarePlugin/APM/ArduCopterFirmwarePlugin.cc index fcd94fc59dbf..fb2b5c9d03e5 100644 --- a/src/FirmwarePlugin/APM/ArduCopterFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/ArduCopterFirmwarePlugin.cc @@ -74,6 +74,92 @@ ArduCopterFirmwarePlugin::ArduCopterFirmwarePlugin(QObject *parent) remapV4_0["TUNE_MIN"] = QStringLiteral("TUNE_LOW"); remapV4_0["TUNE_MAX"] = QStringLiteral("TUNE_HIGH"); + // ArduPilot 4.7: massive parameter rename and SI unit conversion + FirmwarePlugin::remapParamNameMap_t &remapV4_7 = _remapParamName[4][7]; + + // Position controller: PSC_VELXY_* -> PSC_NE_VEL_* + remapV4_7["PSC_NE_VEL_P"] = QStringLiteral("PSC_VELXY_P"); + remapV4_7["PSC_NE_VEL_I"] = QStringLiteral("PSC_VELXY_I"); + remapV4_7["PSC_NE_VEL_D"] = QStringLiteral("PSC_VELXY_D"); + remapV4_7["PSC_NE_VEL_IMAX"] = QStringLiteral("PSC_VELXY_IMAX"); + remapV4_7["PSC_NE_VEL_FLTE"] = QStringLiteral("PSC_VELXY_FLTE"); + remapV4_7["PSC_NE_VEL_FLTD"] = QStringLiteral("PSC_VELXY_FLTD"); + remapV4_7["PSC_NE_VEL_FF"] = QStringLiteral("PSC_VELXY_FF"); + + // Position controller: PSC_VELZ_* -> PSC_D_VEL_* + remapV4_7["PSC_D_VEL_P"] = QStringLiteral("PSC_VELZ_P"); + remapV4_7["PSC_D_VEL_I"] = QStringLiteral("PSC_VELZ_I"); + remapV4_7["PSC_D_VEL_D"] = QStringLiteral("PSC_VELZ_D"); + remapV4_7["PSC_D_VEL_IMAX"] = QStringLiteral("PSC_VELZ_IMAX"); + remapV4_7["PSC_D_VEL_FLTE"] = QStringLiteral("PSC_VELZ_FLTE"); + remapV4_7["PSC_D_VEL_FF"] = QStringLiteral("PSC_VELZ_FF"); + + // Position controller: PSC_ACCZ_* -> PSC_D_ACC_* + remapV4_7["PSC_D_ACC_P"] = QStringLiteral("PSC_ACCZ_P"); + remapV4_7["PSC_D_ACC_I"] = QStringLiteral("PSC_ACCZ_I"); + remapV4_7["PSC_D_ACC_D"] = QStringLiteral("PSC_ACCZ_D"); + remapV4_7["PSC_D_ACC_IMAX"] = QStringLiteral("PSC_ACCZ_IMAX"); + remapV4_7["PSC_D_ACC_FLTD"] = QStringLiteral("PSC_ACCZ_FLTD"); + remapV4_7["PSC_D_ACC_FLTE"] = QStringLiteral("PSC_ACCZ_FLTE"); + remapV4_7["PSC_D_ACC_FLTT"] = QStringLiteral("PSC_ACCZ_FLTT"); + remapV4_7["PSC_D_ACC_FF"] = QStringLiteral("PSC_ACCZ_FF"); + remapV4_7["PSC_D_ACC_SMAX"] = QStringLiteral("PSC_ACCZ_SMAX"); + + // Position controller: PSC_POSXY_P -> PSC_NE_POS_P (simply renamed) + remapV4_7["PSC_NE_POS_P"] = QStringLiteral("PSC_POSXY_P"); + + // Position controller: PSC_POSZ_P -> PSC_D_POS_P (simply renamed) + remapV4_7["PSC_D_POS_P"] = QStringLiteral("PSC_POSZ_P"); + + // Waypoint navigation: WPNAV_* -> WP_* + remapV4_7["WP_ACC"] = QStringLiteral("WPNAV_ACCEL"); + remapV4_7["WP_ACC_CNR"] = QStringLiteral("WPNAV_ACCEL_C"); + remapV4_7["WP_ACC_Z"] = QStringLiteral("WPNAV_ACCEL_Z"); + remapV4_7["WP_RADIUS_M"] = QStringLiteral("WPNAV_RADIUS"); + remapV4_7["WP_SPD"] = QStringLiteral("WPNAV_SPEED"); + remapV4_7["WP_SPD_DN"] = QStringLiteral("WPNAV_SPEED_DN"); + remapV4_7["WP_SPD_UP"] = QStringLiteral("WPNAV_SPEED_UP"); + + // RTL parameters + remapV4_7["RTL_ALT_M"] = QStringLiteral("RTL_ALT"); + remapV4_7["RTL_SPEED_MS"] = QStringLiteral("RTL_SPEED"); + remapV4_7["RTL_ALT_FINAL_M"] = QStringLiteral("RTL_ALT_FINAL"); + remapV4_7["RTL_CLIMB_MIN_M"] = QStringLiteral("RTL_CLIMB_MIN"); + + // Landing parameters + remapV4_7["LAND_SPD_MS"] = QStringLiteral("LAND_SPEED"); + remapV4_7["LAND_SPD_HIGH_MS"]= QStringLiteral("LAND_SPEED_HIGH"); + remapV4_7["LAND_ALT_LOW_M"] = QStringLiteral("LAND_ALT_LOW"); + + // Loiter parameters + remapV4_7["LOIT_SPEED_MS"] = QStringLiteral("LOIT_SPEED"); + remapV4_7["LOIT_ACC_MAX_M"] = QStringLiteral("LOIT_ACC_MAX"); + remapV4_7["LOIT_BRK_ACC_M"] = QStringLiteral("LOIT_BRK_ACCEL"); + remapV4_7["LOIT_BRK_JRK_M"] = QStringLiteral("LOIT_BRK_JERK"); + + // Pilot parameters + remapV4_7["PILOT_ACC_Z"] = QStringLiteral("PILOT_ACCEL_Z"); + remapV4_7["PILOT_SPD_UP"] = QStringLiteral("PILOT_SPEED_UP"); + remapV4_7["PILOT_SPD_DN"] = QStringLiteral("PILOT_SPEED_DN"); + remapV4_7["PILOT_TKO_ALT_M"] = QStringLiteral("PILOT_TKOFF_ALT"); + + // Attitude controller + remapV4_7["ATC_ANGLE_MAX"] = QStringLiteral("ANGLE_MAX"); + remapV4_7["ATC_ACC_R_MAX"] = QStringLiteral("ATC_ACCEL_R_MAX"); + remapV4_7["ATC_ACC_P_MAX"] = QStringLiteral("ATC_ACCEL_P_MAX"); + remapV4_7["ATC_ACC_Y_MAX"] = QStringLiteral("ATC_ACCEL_Y_MAX"); + remapV4_7["ATC_RATE_WPY_MAX"]= QStringLiteral("ATC_SLEW_YAW"); + + // Circle + remapV4_7["CIRCLE_RADIUS_M"] = QStringLiteral("CIRCLE_RADIUS"); + + // PosHold + remapV4_7["PHLD_BRK_ANGLE"] = QStringLiteral("PHLD_BRAKE_ANGLE"); + remapV4_7["PHLD_BRK_RATE"] = QStringLiteral("PHLD_BRAKE_RATE"); + + // EKF + remapV4_7["EK3_FLOW_MAX"] = QStringLiteral("EK3_MAX_FLOW"); + _remapParamNameIntialized = true; } } @@ -85,7 +171,7 @@ ArduCopterFirmwarePlugin::~ArduCopterFirmwarePlugin() int ArduCopterFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int majorVersionNumber) const { - return ((majorVersionNumber == 4) ? 0 : Vehicle::versionNotSetValue); + return ((majorVersionNumber == 4) ? 7 : Vehicle::versionNotSetValue); } bool ArduCopterFirmwarePlugin::multiRotorXConfig(Vehicle *vehicle) const diff --git a/src/FirmwarePlugin/APM/ArduPlaneFirmwarePlugin.cc b/src/FirmwarePlugin/APM/ArduPlaneFirmwarePlugin.cc index f1bdfc591d8c..d5469bdb30ea 100644 --- a/src/FirmwarePlugin/APM/ArduPlaneFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/ArduPlaneFirmwarePlugin.cc @@ -76,6 +76,70 @@ ArduPlaneFirmwarePlugin::ArduPlaneFirmwarePlugin(QObject *parent) remapV4_5["RTL_ALTITUDE"] = QStringLiteral("ALT_HOLD_RTL"); // LAND_SPEED is only used in a Copter component + // ArduPilot 4.7: QuadPlane parameter renames and SI unit conversion + FirmwarePlugin::remapParamNameMap_t &remapV4_7 = _remapParamName[4][7]; + + // Attitude controller + remapV4_7["Q_A_ANGLE_MAX"] = QStringLiteral("Q_ANGLE_MAX"); + remapV4_7["Q_A_ACC_R_MAX"] = QStringLiteral("Q_A_ACCEL_R_MAX"); + remapV4_7["Q_A_ACC_P_MAX"] = QStringLiteral("Q_A_ACCEL_P_MAX"); + remapV4_7["Q_A_ACC_Y_MAX"] = QStringLiteral("Q_A_ACCEL_Y_MAX"); + remapV4_7["Q_A_RATE_WPY_MAX"] = QStringLiteral("Q_A_SLEW_YAW"); + + // Loiter + remapV4_7["Q_LOIT_SPEED_MS"] = QStringLiteral("Q_LOIT_SPEED"); + remapV4_7["Q_LOIT_ACC_MAX_M"] = QStringLiteral("Q_LOIT_ACC_MAX"); + remapV4_7["Q_LOIT_BRK_ACC_M"] = QStringLiteral("Q_LOIT_BRK_ACCEL"); + remapV4_7["Q_LOIT_BRK_JRK_M"] = QStringLiteral("Q_LOIT_BRK_JERK"); + + // Pilot + remapV4_7["Q_PILOT_SPD_UP"] = QStringLiteral("Q_PILOT_SPEED_UP"); + remapV4_7["Q_PILOT_SPD_DN"] = QStringLiteral("Q_PILOT_SPEED_DN"); + remapV4_7["Q_PILOT_TKO_ALT_M"] = QStringLiteral("Q_PILOT_TKOFF_ALT"); + + // Position controller: Q_P_VELXY_* -> Q_P_NE_VEL_* + remapV4_7["Q_P_NE_VEL_P"] = QStringLiteral("Q_P_VELXY_P"); + remapV4_7["Q_P_NE_VEL_I"] = QStringLiteral("Q_P_VELXY_I"); + remapV4_7["Q_P_NE_VEL_D"] = QStringLiteral("Q_P_VELXY_D"); + remapV4_7["Q_P_NE_VEL_IMAX"] = QStringLiteral("Q_P_VELXY_IMAX"); + remapV4_7["Q_P_NE_VEL_FLTE"] = QStringLiteral("Q_P_VELXY_FLTE"); + remapV4_7["Q_P_NE_VEL_FLTD"] = QStringLiteral("Q_P_VELXY_FLTD"); + remapV4_7["Q_P_NE_VEL_FF"] = QStringLiteral("Q_P_VELXY_FF"); + + // Position controller: Q_P_VELZ_* -> Q_P_D_VEL_* + remapV4_7["Q_P_D_VEL_P"] = QStringLiteral("Q_P_VELZ_P"); + remapV4_7["Q_P_D_VEL_I"] = QStringLiteral("Q_P_VELZ_I"); + remapV4_7["Q_P_D_VEL_D"] = QStringLiteral("Q_P_VELZ_D"); + remapV4_7["Q_P_D_VEL_IMAX"] = QStringLiteral("Q_P_VELZ_IMAX"); + remapV4_7["Q_P_D_VEL_FLTE"] = QStringLiteral("Q_P_VELZ_FLTE"); + remapV4_7["Q_P_D_VEL_FF"] = QStringLiteral("Q_P_VELZ_FF"); + + // Position controller: Q_P_ACCZ_* -> Q_P_D_ACC_* + remapV4_7["Q_P_D_ACC_P"] = QStringLiteral("Q_P_ACCZ_P"); + remapV4_7["Q_P_D_ACC_I"] = QStringLiteral("Q_P_ACCZ_I"); + remapV4_7["Q_P_D_ACC_D"] = QStringLiteral("Q_P_ACCZ_D"); + remapV4_7["Q_P_D_ACC_IMAX"] = QStringLiteral("Q_P_ACCZ_IMAX"); + remapV4_7["Q_P_D_ACC_FLTD"] = QStringLiteral("Q_P_ACCZ_FLTD"); + remapV4_7["Q_P_D_ACC_FLTE"] = QStringLiteral("Q_P_ACCZ_FLTE"); + remapV4_7["Q_P_D_ACC_FLTT"] = QStringLiteral("Q_P_ACCZ_FLTT"); + remapV4_7["Q_P_D_ACC_FF"] = QStringLiteral("Q_P_ACCZ_FF"); + remapV4_7["Q_P_D_ACC_SMAX"] = QStringLiteral("Q_P_ACCZ_SMAX"); + + // Waypoint navigation + remapV4_7["Q_WP_ACC"] = QStringLiteral("Q_WP_ACCEL"); + remapV4_7["Q_WP_ACC_CNR"] = QStringLiteral("Q_WP_ACCEL_C"); + remapV4_7["Q_WP_ACC_Z"] = QStringLiteral("Q_WP_ACCEL_Z"); + remapV4_7["Q_WP_RADIUS_M"] = QStringLiteral("Q_WP_RADIUS"); + remapV4_7["Q_WP_SPD"] = QStringLiteral("Q_WP_SPEED"); + remapV4_7["Q_WP_SPD_DN"] = QStringLiteral("Q_WP_SPEED_DN"); + remapV4_7["Q_WP_SPD_UP"] = QStringLiteral("Q_WP_SPEED_UP"); + + // EKF + remapV4_7["EK3_FLOW_MAX"] = QStringLiteral("EK3_MAX_FLOW"); + + // Common + remapV4_7["ARMING_SKIPCHK"] = QStringLiteral("ARMING_CHECK"); + _remapParamNameIntialized = true; } } @@ -87,8 +151,7 @@ ArduPlaneFirmwarePlugin::~ArduPlaneFirmwarePlugin() int ArduPlaneFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int majorVersionNumber) const { - // Remapping supports up to 4.5 - return ((majorVersionNumber == 4) ? 5 : Vehicle::versionNotSetValue); + return ((majorVersionNumber == 4) ? 7 : Vehicle::versionNotSetValue); } QString ArduPlaneFirmwarePlugin::takeOffFlightMode() const diff --git a/src/FirmwarePlugin/APM/ArduRoverFirmwarePlugin.cc b/src/FirmwarePlugin/APM/ArduRoverFirmwarePlugin.cc index eab875cab42e..496bc382c980 100644 --- a/src/FirmwarePlugin/APM/ArduRoverFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/ArduRoverFirmwarePlugin.cc @@ -47,6 +47,15 @@ ArduRoverFirmwarePlugin::ArduRoverFirmwarePlugin(QObject *parent) updateAvailableFlightModes(availableFlightModes); if (!_remapParamNameIntialized) { + // ArduPilot 4.7: parameter renames and SI unit conversion + FirmwarePlugin::remapParamNameMap_t &remapV4_7 = _remapParamName[4][7]; + + // EKF + remapV4_7["EK3_FLOW_MAX"] = QStringLiteral("EK3_MAX_FLOW"); + + // Common + remapV4_7["ARMING_SKIPCHK"] = QStringLiteral("ARMING_CHECK"); + _remapParamNameIntialized = true; } } @@ -56,10 +65,9 @@ ArduRoverFirmwarePlugin::~ArduRoverFirmwarePlugin() } -int ArduRoverFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int /*majorVersionNumber*/) const +int ArduRoverFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int majorVersionNumber) const { - // Remapping not supported - return Vehicle::versionNotSetValue; + return ((majorVersionNumber == 4) ? 7 : Vehicle::versionNotSetValue); } void ArduRoverFirmwarePlugin::guidedModeChangeAltitude(Vehicle* /*vehicle*/, double /*altitudeChange*/, bool /*pauseVehicle*/) diff --git a/src/FirmwarePlugin/APM/ArduSubFirmwarePlugin.cc b/src/FirmwarePlugin/APM/ArduSubFirmwarePlugin.cc index 6e275974c24f..ab066cb578e9 100644 --- a/src/FirmwarePlugin/APM/ArduSubFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/ArduSubFirmwarePlugin.cc @@ -125,6 +125,67 @@ ArduSubFirmwarePlugin::ArduSubFirmwarePlugin(QObject *parent) updateAvailableFlightModes(availableFlightModes); if (!_remapParamNameIntialized) { + // ArduPilot 4.7: parameter renames and SI unit conversion + FirmwarePlugin::remapParamNameMap_t &remapV4_7 = _remapParamName[4][7]; + + // Position controller: PSC_VELXY_* -> PSC_NE_VEL_* + remapV4_7["PSC_NE_VEL_P"] = QStringLiteral("PSC_VELXY_P"); + remapV4_7["PSC_NE_VEL_I"] = QStringLiteral("PSC_VELXY_I"); + remapV4_7["PSC_NE_VEL_D"] = QStringLiteral("PSC_VELXY_D"); + remapV4_7["PSC_NE_VEL_IMAX"] = QStringLiteral("PSC_VELXY_IMAX"); + remapV4_7["PSC_NE_VEL_FLTE"] = QStringLiteral("PSC_VELXY_FLTE"); + remapV4_7["PSC_NE_VEL_FLTD"] = QStringLiteral("PSC_VELXY_FLTD"); + remapV4_7["PSC_NE_VEL_FF"] = QStringLiteral("PSC_VELXY_FF"); + + // Position controller: PSC_VELZ_* -> PSC_D_VEL_* + remapV4_7["PSC_D_VEL_P"] = QStringLiteral("PSC_VELZ_P"); + remapV4_7["PSC_D_VEL_I"] = QStringLiteral("PSC_VELZ_I"); + remapV4_7["PSC_D_VEL_D"] = QStringLiteral("PSC_VELZ_D"); + remapV4_7["PSC_D_VEL_IMAX"] = QStringLiteral("PSC_VELZ_IMAX"); + remapV4_7["PSC_D_VEL_FLTE"] = QStringLiteral("PSC_VELZ_FLTE"); + remapV4_7["PSC_D_VEL_FF"] = QStringLiteral("PSC_VELZ_FF"); + + // Position controller: PSC_ACCZ_* -> PSC_D_ACC_* + remapV4_7["PSC_D_ACC_P"] = QStringLiteral("PSC_ACCZ_P"); + remapV4_7["PSC_D_ACC_I"] = QStringLiteral("PSC_ACCZ_I"); + remapV4_7["PSC_D_ACC_D"] = QStringLiteral("PSC_ACCZ_D"); + remapV4_7["PSC_D_ACC_IMAX"] = QStringLiteral("PSC_ACCZ_IMAX"); + remapV4_7["PSC_D_ACC_FLTD"] = QStringLiteral("PSC_ACCZ_FLTD"); + remapV4_7["PSC_D_ACC_FLTE"] = QStringLiteral("PSC_ACCZ_FLTE"); + remapV4_7["PSC_D_ACC_FLTT"] = QStringLiteral("PSC_ACCZ_FLTT"); + remapV4_7["PSC_D_ACC_FF"] = QStringLiteral("PSC_ACCZ_FF"); + remapV4_7["PSC_D_ACC_SMAX"] = QStringLiteral("PSC_ACCZ_SMAX"); + + // Position controller: PSC_POSXY_P -> PSC_NE_POS_P + remapV4_7["PSC_NE_POS_P"] = QStringLiteral("PSC_POSXY_P"); + + // Position controller: PSC_POSZ_P -> PSC_D_POS_P + remapV4_7["PSC_D_POS_P"] = QStringLiteral("PSC_POSZ_P"); + + // Waypoint navigation: WPNAV_* -> WP_* + remapV4_7["WP_ACC"] = QStringLiteral("WPNAV_ACCEL"); + remapV4_7["WP_ACC_CNR"] = QStringLiteral("WPNAV_ACCEL_C"); + remapV4_7["WP_ACC_Z"] = QStringLiteral("WPNAV_ACCEL_Z"); + remapV4_7["WP_RADIUS_M"] = QStringLiteral("WPNAV_RADIUS"); + remapV4_7["WP_SPD"] = QStringLiteral("WPNAV_SPEED"); + remapV4_7["WP_SPD_DN"] = QStringLiteral("WPNAV_SPEED_DN"); + remapV4_7["WP_SPD_UP"] = QStringLiteral("WPNAV_SPEED_UP"); + + // Attitude controller + remapV4_7["ATC_ACC_R_MAX"] = QStringLiteral("ATC_ACCEL_R_MAX"); + remapV4_7["ATC_ACC_P_MAX"] = QStringLiteral("ATC_ACCEL_P_MAX"); + remapV4_7["ATC_ACC_Y_MAX"] = QStringLiteral("ATC_ACCEL_Y_MAX"); + remapV4_7["ATC_RATE_WPY_MAX"]= QStringLiteral("ATC_SLEW_YAW"); + + // Circle + remapV4_7["CIRCLE_RADIUS_M"] = QStringLiteral("CIRCLE_RADIUS"); + + // EKF + remapV4_7["EK3_FLOW_MAX"] = QStringLiteral("EK3_MAX_FLOW"); + + // Common + remapV4_7["ARMING_SKIPCHK"] = QStringLiteral("ARMING_CHECK"); + _remapParamNameIntialized = true; } @@ -142,10 +203,9 @@ ArduSubFirmwarePlugin::~ArduSubFirmwarePlugin() } -int ArduSubFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int /*majorVersionNumber*/) const +int ArduSubFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int majorVersionNumber) const { - // Remapping not supported - return Vehicle::versionNotSetValue; + return ((majorVersionNumber == 4) ? 7 : Vehicle::versionNotSetValue); } void ArduSubFirmwarePlugin::initializeStreamRates(Vehicle *vehicle) diff --git a/src/FirmwarePlugin/FirmwarePlugin.h b/src/FirmwarePlugin/FirmwarePlugin.h index 06334232d522..56f850495852 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.h +++ b/src/FirmwarePlugin/FirmwarePlugin.h @@ -8,10 +8,10 @@ #include "QGCMAVLink.h" #include "FollowMe.h" #include "FactMetaData.h" +#include "Vehicle.h" class VehicleComponent; class AutoPilotPlugin; -class Vehicle; class MavlinkCameraControlInterface; class QGCCameraManager; class Autotune; @@ -82,14 +82,27 @@ class FirmwarePlugin : public QObject GuidedTakeoffCapability = 1 << 7, ///< Vehicle supports guided takeoff }; - /// Maps from on parameter name to another - /// key: parameter name to translate from - /// value: mapped parameter name + /// Parameter name remapping support: + /// When firmware renames a parameter across versions, callers should use the *newest* (current) + /// parameter name. ParameterManager::_remapParamNameToVersion() walks the remap tables backwards + /// from the highest known minor version down to the vehicle's actual firmware version, translating + /// new names to old names at each step. If the name is not found in any remap table it passes + /// through unchanged, so remapping is always safe to run. + /// + /// To bypass remapping (e.g. when checking parameterExists for a specific old or new name to + /// decide which unit conversion to apply), prefix the name with "noremap.". + /// + /// Remap table entries map new_name -> old_name for the version in which the rename occurred. + /// For example: remapV4_0["TUNE_MIN"] = "TUNE_LOW" means TUNE_LOW was renamed to TUNE_MIN in 4.0. + + /// Maps from one parameter name to another (new_name -> old_name for a given version) + /// key: current (new) parameter name + /// value: previous (old) parameter name typedef QMap remapParamNameMap_t; /// Maps from firmware minor version to remapParamNameMap_t entry - /// key: firmware minor version - /// value: remapParamNameMap_t entry + /// key: firmware minor version in which the rename(s) occurred + /// value: remapParamNameMap_t with new->old mappings for that version typedef QMap remapParamNameMinorVersionRemapMap_t; /// Maps from firmware major version number to remapParamNameMinorVersionRemapMap_t entry @@ -184,7 +197,7 @@ class FirmwarePlugin : public QObject virtual double minimumTakeoffAltitudeMeters(Vehicle* /*vehicle*/) const { return 3.048; } /// @return The maximum horizontal groundspeed for a multirotor. - virtual double maximumHorizontalSpeedMultirotor(Vehicle* /*vehicle*/) const { return NAN; } + virtual double maximumHorizontalSpeedMultirotorMetersSecond(Vehicle* /*vehicle*/) const { return NAN; } /// @return The maximum equivalent airspeed setpoint. virtual double maximumEquivalentAirspeed(Vehicle* /*vehicle*/) const { return NAN; } @@ -306,10 +319,13 @@ class FirmwarePlugin : public QObject virtual QString missionCommandOverrides(QGCMAVLink::VehicleClass_t vehicleClass) const; /// Returns the mapping structure which is used to map from one parameter name to another based on firmware version. + /// See remapParamNameMap_t for details on how remapping works. virtual const remapParamNameMajorVersionMap_t ¶mNameRemapMajorVersionMap() const; - /// Returns the highest major version number that is known to the remap for this specified major version. - virtual int remapParamNameHigestMinorVersionNumber(int /*majorVersionNumber*/) const { return 0; } + /// Returns the highest minor version number that has remap entries for the specified major version. + /// The remap logic iterates backwards from this version down to the vehicle's actual minor version. + /// Return Vehicle::versionNotSetValue if remapping is not supported for the given major version. + virtual int remapParamNameHigestMinorVersionNumber(int /*majorVersionNumber*/) const { return Vehicle::versionNotSetValue; } /// @return true: Motors are coaxial like an X8 config, false: Quadcopter for example virtual bool multiRotorCoaxialMotors(Vehicle* /*vehicle*/) const { return false; } diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc index 644630d1ccca..9bd3349adb37 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc @@ -343,7 +343,7 @@ void PX4FirmwarePlugin::guidedModeTakeoff(Vehicle* vehicle, double takeoffAltRel static_cast(takeoffAltAMSL)); // AMSL altitude } -double PX4FirmwarePlugin::maximumHorizontalSpeedMultirotor(Vehicle* vehicle) const +double PX4FirmwarePlugin::maximumHorizontalSpeedMultirotorMetersSecond(Vehicle* vehicle) const { QString speedParam("MPC_XY_VEL_MAX"); @@ -351,7 +351,7 @@ double PX4FirmwarePlugin::maximumHorizontalSpeedMultirotor(Vehicle* vehicle) con return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, speedParam)->rawValue().toDouble(); } - return FirmwarePlugin::maximumHorizontalSpeedMultirotor(vehicle); + return FirmwarePlugin::maximumHorizontalSpeedMultirotorMetersSecond(vehicle); } double PX4FirmwarePlugin::maximumEquivalentAirspeed(Vehicle* vehicle) const diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h index 760c78efec8b..438c3c53b62a 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h @@ -37,7 +37,7 @@ class PX4FirmwarePlugin : public FirmwarePlugin void guidedModeRTL (Vehicle* vehicle, bool smartRTL) const override; void guidedModeLand (Vehicle* vehicle) const override; void guidedModeTakeoff (Vehicle* vehicle, double takeoffAltRel) const override; - double maximumHorizontalSpeedMultirotor(Vehicle* vehicle) const override; + double maximumHorizontalSpeedMultirotorMetersSecond(Vehicle* vehicle) const override; double maximumEquivalentAirspeed(Vehicle* vehicle) const override; double minimumEquivalentAirspeed(Vehicle* vehicle) const override; bool mulirotorSpeedLimitsAvailable(Vehicle* vehicle) const override; diff --git a/src/FlyView/GuidedActionsController.qml b/src/FlyView/GuidedActionsController.qml index b4679f5e878f..570eab6629e8 100644 --- a/src/FlyView/GuidedActionsController.qml +++ b/src/FlyView/GuidedActionsController.qml @@ -218,8 +218,8 @@ Item { guidedValueSlider.setupSlider( GuidedValueSlider.SliderType.Speed, _unitsConversion.metersSecondToAppSettingsSpeedUnits(0.1).toFixed(1), - _unitsConversion.metersSecondToAppSettingsSpeedUnits(_activeVehicle.maximumHorizontalSpeedMultirotor()).toFixed(1), - _unitsConversion.metersSecondToAppSettingsSpeedUnits(_activeVehicle.maximumHorizontalSpeedMultirotor()/2).toFixed(1), + _unitsConversion.metersSecondToAppSettingsSpeedUnits(_activeVehicle.maximumHorizontalSpeedMultirotorMetersSecond()).toFixed(1), + _unitsConversion.metersSecondToAppSettingsSpeedUnits(_activeVehicle.maximumHorizontalSpeedMultirotorMetersSecond()/2).toFixed(1), qsTr("Speed")) } else { console.error("setupSlider called for inapproproate change speed action", _vehicleInFwdFlight, _activeVehicle.haveMRSpeedLimits) diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index 21ada78bf884..f00c40099099 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -1997,9 +1997,9 @@ double Vehicle::minimumTakeoffAltitudeMeters() return _firmwarePlugin->minimumTakeoffAltitudeMeters(this); } -double Vehicle::maximumHorizontalSpeedMultirotor() +double Vehicle::maximumHorizontalSpeedMultirotorMetersSecond() { - return _firmwarePlugin->maximumHorizontalSpeedMultirotor(this); + return _firmwarePlugin->maximumHorizontalSpeedMultirotorMetersSecond(this); } diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index 30287017594f..952b87267901 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -290,7 +290,7 @@ class Vehicle : public VehicleFactGroup Q_INVOKABLE double minimumTakeoffAltitudeMeters(); /// @return Maximum horizontal speed multirotor. - Q_INVOKABLE double maximumHorizontalSpeedMultirotor(); + Q_INVOKABLE double maximumHorizontalSpeedMultirotorMetersSecond(); /// @return Maximum equivalent airspeed. Q_INVOKABLE double maximumEquivalentAirspeed(); From 0e4e29b50622b9aab5670b191abeb54f53d253f7 Mon Sep 17 00:00:00 2001 From: HTRamsey <68555040+HTRamsey@users.noreply.github.com> Date: Mon, 16 Mar 2026 06:44:05 +0000 Subject: [PATCH 108/119] chore: update PX4 metadata --- .../PX4/AirframeFactMetaData.xml | 62 +- .../PX4/PX4ParameterFactMetaData.xml | 18905 ++++------------ 2 files changed, 4473 insertions(+), 14494 deletions(-) diff --git a/src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml b/src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml index d91b37a0046e..37ec0a18d685 100644 --- a/src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml +++ b/src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml @@ -175,7 +175,7 @@ Copter Beat Kueng <beat-kueng@gmx.net> Quadrotor x - https://docs.px4.io/main/en/frames_multicopter/holybro_qav250_pixhawk4_mini.html + https://docs.px4.io/main/en/frames_multicopter/holybro_qav250_pixhawk4_mini Copter @@ -270,6 +270,10 @@ + + Rover + Rover + Rover Rover @@ -277,7 +281,7 @@ Rover Rover - https://www.aionrobotics.com/r1 + https://docs.px4.io/main/en/complete_vehicles_rover/aion_r1 Rover @@ -288,23 +292,29 @@ Rover https://www.axialadventure.com/product/1-10-scx10-ii-trail-honcho-4wd-rock-crawler-brushed-rtr/AXID9059.html - + Rover Rover - + Rover - Rover - throttle - steering - - Rover - Katrin Moritz - Rover - Speed of left wheels - Steering servo + + + + Spacecraft + DISCOWER + Free-Flyer + https://atmos.discower.io + back left thruster, +x thrust + front left thruster, -x thrust + back right thruster, +x thrust + front right thruster, -x thrust + front left thruster, +y thrust + front right thruster, -y thrust + back left thruster, +y thrust + back right thruster, -y thrust @@ -343,12 +353,32 @@ elevon right elevon left + + VTOL + Simulation + MC motor front right + MC motor back left + MC motor front left + MC motor back right + Forward thrust motor + Ailerons (single channel) + Elevator + Rudder + VTOL Roman Bapst <roman@auterion.com> Standard VTOL + MC motor front right + MC motor back left + MC motor front left + MC motor back right + Forward thrust motor + Aileron + Elevator + Rudder VTOL @@ -372,10 +402,4 @@ VTOL Tiltrotor - - - Spacecraft - Free-Flyer - - diff --git a/src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml b/src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml index 66a3ff1bf333..9c2f190c5a9e 100644 --- a/src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml +++ b/src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml @@ -2,209 +2,17 @@ 3 1 15 - - - Speed controller bandwidth - Speed controller bandwidth, in Hz. Higher values result in faster speed and current rise times, but may result in overshoot and higher current consumption. For fixed-wing aircraft, this value should be less than 50 Hz; for multirotors, values up to 100 Hz may provide improvements in responsiveness. - 10 - 250 - Hz - - - Reverse direction - Motor spin direction as detected during initial enumeration. Use 0 or 1 to reverse direction. - 0 - 1 - - - Speed (RPM) controller gain - Speed (RPM) controller gain. Determines controller - aggressiveness; units are amp-seconds per radian. Systems with - higher rotational inertia (large props) will need gain increased; - systems with low rotational inertia (small props) may need gain - decreased. Higher values result in faster response, but may result - in oscillation and excessive overshoot. Lower values result in a - slower, smoother response. - 0.00 - 1.00 - C/rad - 3 - - - Idle speed (e Hz) - Idle speed (e Hz) - 0.0 - 100.0 - Hz - 3 - - - Spin-up rate (e Hz/s) - Spin-up rate (e Hz/s) - 5 - 1000 - 1/s^2 - - - Index of this ESC in throttle command messages. - Index of this ESC in throttle command messages. - 0 - 15 - - - Extended status ID - Extended status ID - 1 - 1000000 - - - Extended status interval (µs) - Extended status interval (µs) - 0 - 1000000 - us - - - ESC status interval (µs) - ESC status interval (µs) - 1000000 - us - - - Motor current limit in amps - Motor current limit in amps. This determines the maximum - current controller setpoint, as well as the maximum allowable - current setpoint slew rate. This value should generally be set to - the continuous current rating listed in the motor’s specification - sheet, or set equal to the motor’s specified continuous power - divided by the motor voltage limit. - 1 - 80 - A - 3 - - - Motor Kv in RPM per volt - Motor Kv in RPM per volt. This can be taken from the motor’s - specification sheet; accuracy will help control performance but - some deviation from the specified value is acceptable. - 0 - 4000 - rpm/V - - - READ ONLY: Motor inductance in henries. - READ ONLY: Motor inductance in henries. This is measured on start-up. - H - 3 - - - Number of motor poles. - Number of motor poles. Used to convert mechanical speeds to - electrical speeds. This number should be taken from the motor’s - specification sheet. - 2 - 40 - - - READ ONLY: Motor resistance in ohms - READ ONLY: Motor resistance in ohms. This is measured on start-up. When - tuning a new motor, check that this value is approximately equal - to the value shown in the motor’s specification sheet. - Ohm - 3 - - - Acceleration limit (V) - Acceleration limit (V) - 0.01 - 1.00 - V - 3 - - - Motor voltage limit in volts - Motor voltage limit in volts. The current controller’s - commanded voltage will never exceed this value. Note that this may - safely be above the nominal voltage of the motor; to determine the - actual motor voltage limit, divide the motor’s rated power by the - motor current limit. - 0 - V - 3 - - - - - GNSS dynamic model - Dynamic model used in the GNSS positioning engine. 0 – - Automotive, 1 – Sea, 2 – Airborne. - - 0 - 2 - - Automotive - Sea - Airborne - - - - Broadcast old GNSS fix message - Broadcast the old (deprecated) GNSS fix message - uavcan.equipment.gnss.Fix alongside the new alternative - uavcan.equipment.gnss.Fix2. It is recommended to - disable this feature to reduce the CAN bus traffic. - - 0 - 1 - - Fix2 - Fix and Fix2 - - - - device health warning - Set the device health to Warning if the dimensionality of - the GNSS solution is less than this value. 3 for the full (3D) - solution, 2 for planar (2D) solution, 1 for time-only solution, - 0 disables the feature. - - 0 - 3 - - disables the feature - time-only solution - planar (2D) solution - full (3D) solution - - - - - Set the device health to Warning if the number of satellites - used in the GNSS solution is below this threshold. Zero - disables the feature - - - - - Set the device health to Warning if the number of satellites - used in the GNSS solution is below this threshold. Zero - disables the feature - - 0 - 1000000 - us - - First 4 characters of CALLSIGN - Sets first 4 characters of a total of 8. Valid characters are A-Z, 0-9, " ". Example "PX4 " -> 1347957792 For CALLSIGN shorter than 8 characters use the null terminator at the end '\0'. + Sets first 4 characters of a total of 8. Valid characters are A-Z, 0-9, " ". Example "PX4 " -> 1347957792 +For CALLSIGN shorter than 8 characters use the null terminator at the end '\0'. true Second 4 characters of CALLSIGN - Sets second 4 characters of a total of 8. Valid characters are A-Z, 0-9, " " only. Example "TEST" -> 1413829460 For CALLSIGN shorter than 8 characters use the null terminator at the end '\0'. + Sets second 4 characters of a total of 8. Valid characters are A-Z, 0-9, " " only. Example "TEST" -> 1413829460 +For CALLSIGN shorter than 8 characters use the null terminator at the end '\0'. true @@ -356,225 +164,15 @@ - - PCA9685 Output Channel 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 10 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 11 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 12 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 13 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 14 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 15 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 16 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 9 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - Put the selected channels into Duty-Cycle output mode - The driver will output standard pulse-width encoded signal without this bit set. To make PCA9685 output in duty-cycle fashion, please enable the corresponding channel bit here and adjusting standard params to suit your need. The driver will have 12bits resolution for duty-cycle output. That means to achieve 0% to 100% output range on one channel, the corresponding params MIN and MAX for the channel should be set to 0 and 4096. Other standard params follows the same rule. - 0 - 65535 - - Put CH1 to Duty-Cycle mode - Put CH2 to Duty-Cycle mode - Put CH3 to Duty-Cycle mode - Put CH4 to Duty-Cycle mode - Put CH5 to Duty-Cycle mode - Put CH6 to Duty-Cycle mode - Put CH7 to Duty-Cycle mode - Put CH8 to Duty-Cycle mode - Put CH9 to Duty-Cycle mode - Put CH10 to Duty-Cycle mode - Put CH11 to Duty-Cycle mode - Put CH12 to Duty-Cycle mode - Put CH13 to Duty-Cycle mode - Put CH14 to Duty-Cycle mode - Put CH15 to Duty-Cycle mode - Put CH16 to Duty-Cycle mode - - - - PCA9685 Output Channel 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC1). - -1 - 2200 - - - PCA9685 Output Channel 10 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC10). - -1 - 2200 - - - PCA9685 Output Channel 11 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC11). - -1 - 2200 - - - PCA9685 Output Channel 12 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC12). - -1 - 2200 - - - PCA9685 Output Channel 13 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC13). - -1 - 2200 - - - PCA9685 Output Channel 14 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC14). - -1 - 2200 - - - PCA9685 Output Channel 15 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC15). - -1 - 2200 - - - PCA9685 Output Channel 16 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC16). - -1 - 2200 - - - PCA9685 Output Channel 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC2). - -1 - 2200 - - - PCA9685 Output Channel 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC3). - -1 - 2200 - - - PCA9685 Output Channel 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC4). - -1 - 2200 - - - PCA9685 Output Channel 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC5). - -1 - 2200 - - - PCA9685 Output Channel 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC6). - -1 - 2200 - - - PCA9685 Output Channel 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC7). - -1 - 2200 - - - PCA9685 Output Channel 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC8). - -1 - 2200 - - - PCA9685 Output Channel 9 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC9). - -1 - 2200 - - - PCA9685 Output Channel 1 Output Function - Select what should be output on PCA9685 Output Channel 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 1 Output Function + Select what should be output on SIM Channel 1. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -623,11 +221,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 10 Output Function - Select what should be output on PCA9685 Output Channel 10. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 10 Output Function + Select what should be output on SIM Channel 10. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -676,11 +284,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 11 Output Function - Select what should be output on PCA9685 Output Channel 11. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 11 Output Function + Select what should be output on SIM Channel 11. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -729,11 +347,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 12 Output Function - Select what should be output on PCA9685 Output Channel 12. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 12 Output Function + Select what should be output on SIM Channel 12. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -782,11 +410,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 13 Output Function - Select what should be output on PCA9685 Output Channel 13. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 13 Output Function + Select what should be output on SIM Channel 13. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -835,11 +473,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 14 Output Function - Select what should be output on PCA9685 Output Channel 14. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 14 Output Function + Select what should be output on SIM Channel 14. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -888,11 +536,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 15 Output Function - Select what should be output on PCA9685 Output Channel 15. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 15 Output Function + Select what should be output on SIM Channel 15. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -941,11 +599,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 16 Output Function - Select what should be output on PCA9685 Output Channel 16. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 16 Output Function + Select what should be output on SIM Channel 16. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -994,11 +662,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 2 Output Function - Select what should be output on PCA9685 Output Channel 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 2 Output Function + Select what should be output on SIM Channel 2. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1047,11 +725,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 3 Output Function - Select what should be output on PCA9685 Output Channel 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 3 Output Function + Select what should be output on SIM Channel 3. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1100,11 +788,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 4 Output Function - Select what should be output on PCA9685 Output Channel 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 4 Output Function + Select what should be output on SIM Channel 4. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1153,11 +851,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 5 Output Function - Select what should be output on PCA9685 Output Channel 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 5 Output Function + Select what should be output on SIM Channel 5. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1206,11 +914,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 6 Output Function - Select what should be output on PCA9685 Output Channel 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 6 Output Function + Select what should be output on SIM Channel 6. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1259,11 +977,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 7 Output Function - Select what should be output on PCA9685 Output Channel 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 7 Output Function + Select what should be output on SIM Channel 7. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1312,11 +1040,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 8 Output Function - Select what should be output on PCA9685 Output Channel 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 8 Output Function + Select what should be output on SIM Channel 8. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1365,11 +1103,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 9 Output Function - Select what should be output on PCA9685 Output Channel 9. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 9 Output Function + Select what should be output on SIM Channel 9. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1418,8173 +1166,93 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 1 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 10 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 11 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 12 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 13 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 14 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 15 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 16 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 2 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 3 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 4 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 5 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 6 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 7 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 8 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 9 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 1 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 10 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 11 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 12 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 13 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 14 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 15 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 16 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 2 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 3 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 4 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 5 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 6 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 7 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 8 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 9 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM cycle frequency - Controls the PWM frequency at timing perspective. This is independent from PWM update frequency, as PCA9685 is capable to output without being continuously commanded by FC. Higher frequency leads to more accurate pulse width, but some ESCs and servos may not support it. This parameter should be set to the same value as PWM update rate in most case. This parameter MUST NOT exceed upper limit of 400.0, if any outputs as generic 1000~2000us pulse width is desired. Frequency higher than 400 only makes sense in duty-cycle mode. - 23.8 - 1525.87 - 2 - - - Reverse Output Range for PCA9685 Output - Allows to reverse the output range for each channel. Note: this is only useful for servos. + + Reverse Output Range for SIM + Allows to reverse the output range for each channel. +Note: this is only useful for servos. 0 65535 - PCA9685 Output Channel 1 - PCA9685 Output Channel 2 - PCA9685 Output Channel 3 - PCA9685 Output Channel 4 - PCA9685 Output Channel 5 - PCA9685 Output Channel 6 - PCA9685 Output Channel 7 - PCA9685 Output Channel 8 - PCA9685 Output Channel 9 - PCA9685 Output Channel 10 - PCA9685 Output Channel 11 - PCA9685 Output Channel 12 - PCA9685 Output Channel 13 - PCA9685 Output Channel 14 - PCA9685 Output Channel 15 - PCA9685 Output Channel 16 + SIM Channel 1 + SIM Channel 2 + SIM Channel 3 + SIM Channel 4 + SIM Channel 5 + SIM Channel 6 + SIM Channel 7 + SIM Channel 8 + SIM Channel 9 + SIM Channel 10 + SIM Channel 11 + SIM Channel 12 + SIM Channel 13 + SIM Channel 14 + SIM Channel 15 + SIM Channel 16 - - PWM update rate - Controls the update rate of PWM output. Flight Controller will inform those numbers of update events in a second, to PCA9685. Higher update rate will consume more I2C bandwidth, which may even lead to worse output latency, or completely block I2C bus. - 50.0 - 400.0 - 2 - - - PWM Aux 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 + + + + Gate size for sideslip angle fusion + Sets the number of standard deviations used by the innovation consistency test. + 1 + 5 + SD - - PWM Capture 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 + + Wind estimator sideslip measurement noise + Sideslip measurement noise of the internal wind estimator(s) of the airspeed selector. + 0 + 1 + rad + 3 - - PWM Capture 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 + + Enable checks on airspeed sensors + Controls which checks are run to check airspeed data for validity. Only applied if ASPD_PRIMARY > 0. +Note: The missing data check (bit 0) is implicitly always enabled when ASPD_DO_CHECKS > 0, even if bit 0 is not explicitly set. + 0 + 31 + + Only data missing check (triggers if more than 1s no data) + Data stuck (triggers if data is exactly constant for 2s in FW mode) + Innovation check (see ASPD_FS_INNOV) + Load factor check (triggers if measurement is below stall speed) + First principle check (airspeed change vs. throttle and pitch) + - - PWM Aux 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 + + Fallback options + + Fallback only to other airspeed sensors + Fallback to groundspeed-minus-windspeed airspeed estimation + Fallback to thrust based airspeed estimation + - - PWM Aux 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Capture 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC1). - -1 - 2200 - - - PWM Capture 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC2). - -1 - 2200 - - - PWM Capture 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC3). - -1 - 2200 - - - PWM Aux 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC2). - -1 - 2200 - - - PWM Aux 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC3). - -1 - 2200 - - - PWM Aux 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC4). - -1 - 2200 - - - PWM Aux 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC5). - -1 - 2200 - - - PWM Aux 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC6). - -1 - 2200 - - - PWM Aux 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC7). - -1 - 2200 - - - PWM Aux 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC8). - -1 - 2200 - - - PWM Capture 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC1). - -1 - 2200 - - - PWM Aux 1 Output Function - Select what should be output on PWM Aux 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Capture 2 Output Function - Select what should be output on PWM Capture 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Capture 3 Output Function - Select what should be output on PWM Capture 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 2 Output Function - Select what should be output on PWM Aux 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 3 Output Function - Select what should be output on PWM Aux 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 4 Output Function - Select what should be output on PWM Aux 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 5 Output Function - Select what should be output on PWM Aux 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 6 Output Function - Select what should be output on PWM Aux 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 7 Output Function - Select what should be output on PWM Aux 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 8 Output Function - Select what should be output on PWM Aux 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Capture 1 Output Function - Select what should be output on PWM Capture 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 1 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Capture 2 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Capture 3 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 2 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 3 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 4 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 5 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 6 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 7 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 8 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Capture 1 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 1 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Capture 2 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Capture 3 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 2 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 3 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 4 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 5 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 6 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 7 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 8 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Capture 1 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - Reverse Output Range for PWM AUX - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 2047 - - PWM Aux 1 - PWM Aux 2 - PWM Aux 3 - PWM Aux 4 - PWM Aux 5 - PWM Aux 6 - PWM Aux 7 - PWM Aux 8 - PWM Capture 1 - PWM Capture 2 - PWM Capture 3 - - - - Output Protocol Configuration for PWM Aux 1-4 - Select which Output Protocol to use for outputs PWM Aux 1-4. Custom PWM rates can be used by directly setting any value >0. - True - - DShot150 - DShot300 - DShot600 - DShot1200 - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for PWM Aux 5-6 - Select which Output Protocol to use for outputs PWM Aux 5-6. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for PWM Aux 7-8 - Select which Output Protocol to use for outputs PWM Aux 7-8. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for PWM Capture 1-3 - Select which Output Protocol to use for outputs PWM Capture 1-3. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - MAIN 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC1). - -1 - 2200 - - - MAIN 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC2). - -1 - 2200 - - - MAIN 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC3). - -1 - 2200 - - - MAIN 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC4). - -1 - 2200 - - - MAIN 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC5). - -1 - 2200 - - - MAIN 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC6). - -1 - 2200 - - - MAIN 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC7). - -1 - 2200 - - - MAIN 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC8). - -1 - 2200 - - - MAIN 1 Output Function - Select what should be output on MAIN 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 2 Output Function - Select what should be output on MAIN 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 3 Output Function - Select what should be output on MAIN 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 4 Output Function - Select what should be output on MAIN 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 5 Output Function - Select what should be output on MAIN 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 6 Output Function - Select what should be output on MAIN 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 7 Output Function - Select what should be output on MAIN 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 8 Output Function - Select what should be output on MAIN 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 1 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 2 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 3 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 4 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 5 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 6 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 7 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 8 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 1 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 2 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 3 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 4 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 5 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 6 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 7 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 8 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - Reverse Output Range for PWM MAIN - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - MAIN 1 - MAIN 2 - MAIN 3 - MAIN 4 - MAIN 5 - MAIN 6 - MAIN 7 - MAIN 8 - - - - Output Protocol Configuration for MAIN 1-2 - Select which Output Protocol to use for outputs MAIN 1-2. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for MAIN 3-4 - Select which Output Protocol to use for outputs MAIN 3-4. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for MAIN 5-8 - Select which Output Protocol to use for outputs MAIN 5-8. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Roboclaw Driver Channel 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 128 - 128 - - - Roboclaw Driver Channel 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 128 - 128 - - - Roboclaw Driver Channel 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see RBCLW_FUNC1). - -1 - 257 - - - Roboclaw Driver Channel 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see RBCLW_FUNC2). - -1 - 257 - - - Roboclaw Driver Channel 1 Output Function - Select what should be output on Roboclaw Driver Channel 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Roboclaw Driver Channel 2 Output Function - Select what should be output on Roboclaw Driver Channel 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Roboclaw Driver Channel 1 Maximum Value - Maxmimum output value (when not disarmed). - 128 - 256 - - - Roboclaw Driver Channel 2 Maximum Value - Maxmimum output value (when not disarmed). - 128 - 256 - - - Roboclaw Driver Channel 1 Minimum Value - Minimum output value (when not disarmed). - 1 - 128 - - - Roboclaw Driver Channel 2 Minimum Value - Minimum output value (when not disarmed). - 1 - 128 - - - Reverse Output Range for Roboclaw Driver - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 3 - - Roboclaw Driver Channel 1 - Roboclaw Driver Channel 2 - - - - SIM_GZ ESC 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC1). - -1 - 1000 - - - SIM_GZ ESC 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC2). - -1 - 1000 - - - SIM_GZ ESC 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC3). - -1 - 1000 - - - SIM_GZ ESC 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC4). - -1 - 1000 - - - SIM_GZ ESC 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC5). - -1 - 1000 - - - SIM_GZ ESC 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC6). - -1 - 1000 - - - SIM_GZ ESC 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC7). - -1 - 1000 - - - SIM_GZ ESC 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC8). - -1 - 1000 - - - SIM_GZ ESC 1 Output Function - Select what should be output on SIM_GZ ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 2 Output Function - Select what should be output on SIM_GZ ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 3 Output Function - Select what should be output on SIM_GZ ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 4 Output Function - Select what should be output on SIM_GZ ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 5 Output Function - Select what should be output on SIM_GZ ESC 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 6 Output Function - Select what should be output on SIM_GZ ESC 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 7 Output Function - Select what should be output on SIM_GZ ESC 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 8 Output Function - Select what should be output on SIM_GZ ESC 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - Reverse Output Range for SIM_GZ - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - SIM_GZ ESC 1 - SIM_GZ ESC 2 - SIM_GZ ESC 3 - SIM_GZ ESC 4 - SIM_GZ ESC 5 - SIM_GZ ESC 6 - SIM_GZ ESC 7 - SIM_GZ ESC 8 - - - - SIM_GZ Servo 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC1). - -1 - 1000 - - - SIM_GZ Servo 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC2). - -1 - 1000 - - - SIM_GZ Servo 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC3). - -1 - 1000 - - - SIM_GZ Servo 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC4). - -1 - 1000 - - - SIM_GZ Servo 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC5). - -1 - 1000 - - - SIM_GZ Servo 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC6). - -1 - 1000 - - - SIM_GZ Servo 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC7). - -1 - 1000 - - - SIM_GZ Servo 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC8). - -1 - 1000 - - - SIM_GZ Servo 1 Output Function - Select what should be output on SIM_GZ Servo 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 2 Output Function - Select what should be output on SIM_GZ Servo 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 3 Output Function - Select what should be output on SIM_GZ Servo 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 4 Output Function - Select what should be output on SIM_GZ Servo 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 5 Output Function - Select what should be output on SIM_GZ Servo 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 6 Output Function - Select what should be output on SIM_GZ Servo 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 7 Output Function - Select what should be output on SIM_GZ Servo 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 8 Output Function - Select what should be output on SIM_GZ Servo 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - Reverse Output Range for SIM_GZ - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - SIM_GZ Servo 1 - SIM_GZ Servo 2 - SIM_GZ Servo 3 - SIM_GZ Servo 4 - SIM_GZ Servo 5 - SIM_GZ Servo 6 - SIM_GZ Servo 7 - SIM_GZ Servo 8 - - - - SIM_GZ Wheels 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 200 - - - SIM_GZ Wheels 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 200 - - - SIM_GZ Wheels 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 200 - - - SIM_GZ Wheels 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 200 - - - SIM_GZ Wheels 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_WH_FUNC1). - -1 - 200 - - - SIM_GZ Wheels 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_WH_FUNC2). - -1 - 200 - - - SIM_GZ Wheels 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_WH_FUNC3). - -1 - 200 - - - SIM_GZ Wheels 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_WH_FUNC4). - -1 - 200 - - - SIM_GZ Wheels 1 Output Function - Select what should be output on SIM_GZ Wheels 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Wheels 2 Output Function - Select what should be output on SIM_GZ Wheels 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Wheels 3 Output Function - Select what should be output on SIM_GZ Wheels 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Wheels 4 Output Function - Select what should be output on SIM_GZ Wheels 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Wheels 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 200 - - - Reverse Output Range for SIM_GZ - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 15 - - SIM_GZ Wheels 1 - SIM_GZ Wheels 2 - SIM_GZ Wheels 3 - SIM_GZ Wheels 4 - - - - TAP ESC Output ESC 1 Output Function - Select what should be output on TAP ESC Output ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 2 Output Function - Select what should be output on TAP ESC Output ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 3 Output Function - Select what should be output on TAP ESC Output ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 4 Output Function - Select what should be output on TAP ESC Output ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 5 Output Function - Select what should be output on TAP ESC Output ESC 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 6 Output Function - Select what should be output on TAP ESC Output ESC 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 7 Output Function - Select what should be output on TAP ESC Output ESC 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 8 Output Function - Select what should be output on TAP ESC Output ESC 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Reverse Output Range for TAP ESC Output - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - TAP ESC Output ESC 1 - TAP ESC Output ESC 2 - TAP ESC Output ESC 3 - TAP ESC Output ESC 4 - TAP ESC Output ESC 5 - TAP ESC Output ESC 6 - TAP ESC Output ESC 7 - TAP ESC Output ESC 8 - - - - UAVCAN ESC 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC1). - -1 - 8191 - - - UAVCAN ESC 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC2). - -1 - 8191 - - - UAVCAN ESC 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC3). - -1 - 8191 - - - UAVCAN ESC 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC4). - -1 - 8191 - - - UAVCAN ESC 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC5). - -1 - 8191 - - - UAVCAN ESC 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC6). - -1 - 8191 - - - UAVCAN ESC 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC7). - -1 - 8191 - - - UAVCAN ESC 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC8). - -1 - 8191 - - - UAVCAN ESC 1 Output Function - Select what should be output on UAVCAN ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 2 Output Function - Select what should be output on UAVCAN ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 3 Output Function - Select what should be output on UAVCAN ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 4 Output Function - Select what should be output on UAVCAN ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 5 Output Function - Select what should be output on UAVCAN ESC 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 6 Output Function - Select what should be output on UAVCAN ESC 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 7 Output Function - Select what should be output on UAVCAN ESC 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 8 Output Function - Select what should be output on UAVCAN ESC 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - Reverse Output Range for UAVCAN - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - UAVCAN ESC 1 - UAVCAN ESC 2 - UAVCAN ESC 3 - UAVCAN ESC 4 - UAVCAN ESC 5 - UAVCAN ESC 6 - UAVCAN ESC 7 - UAVCAN ESC 8 - - - - UAVCAN Servo 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC1). - -1 - 1000 - - - UAVCAN Servo 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC2). - -1 - 1000 - - - UAVCAN Servo 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC3). - -1 - 1000 - - - UAVCAN Servo 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC4). - -1 - 1000 - - - UAVCAN Servo 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC5). - -1 - 1000 - - - UAVCAN Servo 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC6). - -1 - 1000 - - - UAVCAN Servo 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC7). - -1 - 1000 - - - UAVCAN Servo 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC8). - -1 - 1000 - - - UAVCAN Servo 1 Output Function - Select what should be output on UAVCAN Servo 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 2 Output Function - Select what should be output on UAVCAN Servo 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 3 Output Function - Select what should be output on UAVCAN Servo 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 4 Output Function - Select what should be output on UAVCAN Servo 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 5 Output Function - Select what should be output on UAVCAN Servo 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 6 Output Function - Select what should be output on UAVCAN Servo 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 7 Output Function - Select what should be output on UAVCAN Servo 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 8 Output Function - Select what should be output on UAVCAN Servo 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - Reverse Output Range for UAVCAN - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - UAVCAN Servo 1 - UAVCAN Servo 2 - UAVCAN Servo 3 - UAVCAN Servo 4 - UAVCAN Servo 5 - UAVCAN Servo 6 - UAVCAN Servo 7 - UAVCAN Servo 8 - - - - UAVCANv1 ESC 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC1). - -1 - 8191 - - - UAVCANv1 ESC 10 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC10). - -1 - 8191 - - - UAVCANv1 ESC 11 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC11). - -1 - 8191 - - - UAVCANv1 ESC 12 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC12). - -1 - 8191 - - - UAVCANv1 ESC 13 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC13). - -1 - 8191 - - - UAVCANv1 ESC 14 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC14). - -1 - 8191 - - - UAVCANv1 ESC 15 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC15). - -1 - 8191 - - - UAVCANv1 ESC 16 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC16). - -1 - 8191 - - - UAVCANv1 ESC 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC2). - -1 - 8191 - - - UAVCANv1 ESC 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC3). - -1 - 8191 - - - UAVCANv1 ESC 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC4). - -1 - 8191 - - - UAVCANv1 ESC 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC5). - -1 - 8191 - - - UAVCANv1 ESC 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC6). - -1 - 8191 - - - UAVCANv1 ESC 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC7). - -1 - 8191 - - - UAVCANv1 ESC 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC8). - -1 - 8191 - - - UAVCANv1 ESC 9 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC9). - -1 - 8191 - - - UAVCANv1 ESC 1 Output Function - Select what should be output on UAVCANv1 ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 10 Output Function - Select what should be output on UAVCANv1 ESC 10. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 11 Output Function - Select what should be output on UAVCANv1 ESC 11. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 12 Output Function - Select what should be output on UAVCANv1 ESC 12. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 13 Output Function - Select what should be output on UAVCANv1 ESC 13. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 14 Output Function - Select what should be output on UAVCANv1 ESC 14. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 15 Output Function - Select what should be output on UAVCANv1 ESC 15. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 16 Output Function - Select what should be output on UAVCANv1 ESC 16. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 2 Output Function - Select what should be output on UAVCANv1 ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 3 Output Function - Select what should be output on UAVCANv1 ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 4 Output Function - Select what should be output on UAVCANv1 ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 5 Output Function - Select what should be output on UAVCANv1 ESC 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 6 Output Function - Select what should be output on UAVCANv1 ESC 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 7 Output Function - Select what should be output on UAVCANv1 ESC 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 8 Output Function - Select what should be output on UAVCANv1 ESC 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 9 Output Function - Select what should be output on UAVCANv1 ESC 9. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 10 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 11 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 12 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 13 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 14 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 15 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 16 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 9 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 10 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 11 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 12 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 13 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 14 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 15 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 16 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 9 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - Reverse Output Range for UAVCANv1 - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 65535 - - UAVCANv1 ESC 1 - UAVCANv1 ESC 2 - UAVCANv1 ESC 3 - UAVCANv1 ESC 4 - UAVCANv1 ESC 5 - UAVCANv1 ESC 6 - UAVCANv1 ESC 7 - UAVCANv1 ESC 8 - UAVCANv1 ESC 9 - UAVCANv1 ESC 10 - UAVCANv1 ESC 11 - UAVCANv1 ESC 12 - UAVCANv1 ESC 13 - UAVCANv1 ESC 14 - UAVCANv1 ESC 15 - UAVCANv1 ESC 16 - - - - VOXL2 IO Output PWM Channel 1 Output Function - Select what should be output on VOXL2 IO Output PWM Channel 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL2 IO Output PWM Channel 2 Output Function - Select what should be output on VOXL2 IO Output PWM Channel 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL2 IO Output PWM Channel 3 Output Function - Select what should be output on VOXL2 IO Output PWM Channel 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL2 IO Output PWM Channel 4 Output Function - Select what should be output on VOXL2 IO Output PWM Channel 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Reverse Output Range for VOXL2 IO Output - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 15 - - VOXL2 IO Output PWM Channel 1 - VOXL2 IO Output PWM Channel 2 - VOXL2 IO Output PWM Channel 3 - VOXL2 IO Output PWM Channel 4 - - - - VOXL ESC Output ESC 1 Output Function - Select what should be output on VOXL ESC Output ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL ESC Output ESC 2 Output Function - Select what should be output on VOXL ESC Output ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL ESC Output ESC 3 Output Function - Select what should be output on VOXL ESC Output ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL ESC Output ESC 4 Output Function - Select what should be output on VOXL ESC Output ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Reverse Output Range for VOXL ESC Output - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 15 - - VOXL ESC Output ESC 1 - VOXL ESC Output ESC 2 - VOXL ESC Output ESC 3 - VOXL ESC Output ESC 4 - - - - Vertiq IO CVI 0 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 10 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 11 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 12 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 13 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 14 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 15 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 9 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 0 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC0). - -1 - 500 - - - Vertiq IO CVI 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC1). - -1 - 500 - - - Vertiq IO CVI 10 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC10). - -1 - 500 - - - Vertiq IO CVI 11 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC11). - -1 - 500 - - - Vertiq IO CVI 12 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC12). - -1 - 500 - - - Vertiq IO CVI 13 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC13). - -1 - 500 - - - Vertiq IO CVI 14 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC14). - -1 - 500 - - - Vertiq IO CVI 15 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC15). - -1 - 500 - - - Vertiq IO CVI 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC2). - -1 - 500 - - - Vertiq IO CVI 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC3). - -1 - 500 - - - Vertiq IO CVI 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC4). - -1 - 500 - - - Vertiq IO CVI 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC5). - -1 - 500 - - - Vertiq IO CVI 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC6). - -1 - 500 - - - Vertiq IO CVI 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC7). - -1 - 500 - - - Vertiq IO CVI 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC8). - -1 - 500 - - - Vertiq IO CVI 9 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC9). - -1 - 500 - - - Vertiq IO CVI 0 Output Function - Select what should be output on Vertiq IO CVI 0. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 1 Output Function - Select what should be output on Vertiq IO CVI 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 10 Output Function - Select what should be output on Vertiq IO CVI 10. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 11 Output Function - Select what should be output on Vertiq IO CVI 11. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 12 Output Function - Select what should be output on Vertiq IO CVI 12. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 13 Output Function - Select what should be output on Vertiq IO CVI 13. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 14 Output Function - Select what should be output on Vertiq IO CVI 14. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 15 Output Function - Select what should be output on Vertiq IO CVI 15. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 2 Output Function - Select what should be output on Vertiq IO CVI 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 3 Output Function - Select what should be output on Vertiq IO CVI 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 4 Output Function - Select what should be output on Vertiq IO CVI 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 5 Output Function - Select what should be output on Vertiq IO CVI 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 6 Output Function - Select what should be output on Vertiq IO CVI 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 7 Output Function - Select what should be output on Vertiq IO CVI 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 8 Output Function - Select what should be output on Vertiq IO CVI 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 9 Output Function - Select what should be output on Vertiq IO CVI 9. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 0 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 10 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 11 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 12 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 13 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 14 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 15 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 9 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 0 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 10 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 11 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 12 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 13 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 14 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 15 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 9 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Reverse Output Range for Vertiq IO - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 32767 - - Vertiq IO CVI 0 - Vertiq IO CVI 1 - Vertiq IO CVI 2 - Vertiq IO CVI 3 - Vertiq IO CVI 4 - Vertiq IO CVI 5 - Vertiq IO CVI 6 - Vertiq IO CVI 7 - Vertiq IO CVI 8 - Vertiq IO CVI 9 - Vertiq IO CVI 10 - Vertiq IO CVI 11 - Vertiq IO CVI 12 - Vertiq IO CVI 13 - Vertiq IO CVI 14 - Vertiq IO CVI 15 - - - - - - Gate size for sideslip angle fusion - Sets the number of standard deviations used by the innovation consistency test. - 1 - 5 - SD - - - Wind estimator sideslip measurement noise - Sideslip measurement noise of the internal wind estimator(s) of the airspeed selector. - 0 - 1 - rad - 3 - - - Enable checks on airspeed sensors - Controls which checks are run to check airspeed data for validity. Only applied if ASPD_PRIMARY > 0. - 0 - 31 - - Only data missing check (triggers if more than 1s no data) - Data stuck (triggers if data is exactly constant for 2s in FW mode) - Innovation check (see ASPD_FS_INNOV) - Load factor check (triggers if measurement is below stall speed) - First principle check (airspeed change vs. throttle and pitch) - - - - Enable fallback to sensor-less airspeed estimation - If set to true and airspeed checks are enabled, it will use a sensor-less airspeed estimation based on groundspeed minus windspeed if no other airspeed sensor available to fall back to. - - Disable fallback to sensor-less estimation - Enable fallback to sensor-less estimation - - - - First principle airspeed check time window - Window for comparing airspeed change to throttle and pitch change. Triggers when the airspeed change within this window is negative while throttle increases and the vehicle pitches down. Is meant to catch degrading airspeed blockages as can happen when flying through icing conditions. Relies on FW_THR_TRIM being set accurately. - 0 - s - 1 + + First principle airspeed check time window + Window for comparing airspeed change to throttle and pitch change. +Triggers when the airspeed change within this window is negative while throttle increases +and the vehicle pitches down. +Is meant to catch degrading airspeed blockages as can happen when flying through icing conditions. +Relies on FW_THR_TRIM being set accurately. + 0 + s + 1 Airspeed failure innovation threshold - This specifies the minimum airspeed innovation required to trigger a failsafe. Larger values make the check less sensitive, smaller values make it more sensitive. Large innovations indicate an inconsistency between predicted (groundspeed - windspeeed) and measured airspeed. The time required to detect a fault when the threshold is exceeded depends on the size of the exceedance and is controlled by the ASPD_FS_INTEG parameter. + This specifies the minimum airspeed innovation required to trigger a failsafe. Larger values make the check less sensitive, +smaller values make it more sensitive. Large innovations indicate an inconsistency between predicted (groundspeed - windspeeed) +and measured airspeed. +The time required to detect a fault when the threshold is exceeded depends on the size of the exceedance and is controlled by the ASPD_FS_INTEG parameter. 0.5 10.0 m/s @@ -9592,7 +1260,8 @@ Airspeed failure innovation integral threshold - This sets the time integral of airspeed innovation exceedance above ASPD_FS_INNOV required to trigger a failsafe. Larger values make the check less sensitive, smaller positive values make it more sensitive. + This sets the time integral of airspeed innovation exceedance above ASPD_FS_INNOV required to trigger a failsafe. +Larger values make the check less sensitive, smaller positive values make it more sensitive. 0.0 50.0 m @@ -9600,7 +1269,8 @@ Airspeed failsafe start delay - Delay before switching back to using airspeed sensor if checks indicate sensor is good. Set to a negative value to disable the re-enabling in flight. + Delay before switching back to using airspeed sensor if checks indicate sensor is good. +Set to a negative value to disable the re-enabling in flight. -1.0 s 1 @@ -9620,6 +1290,7 @@ First airspeed sensor Second airspeed sensor Third airspeed sensor + Thrust based airspeed @@ -9628,7 +1299,6 @@ 0.5 2.0 2 - true Scale of airspeed sensor 2 @@ -9636,7 +1306,6 @@ 0.5 2.0 2 - true Scale of airspeed sensor 3 @@ -9644,7 +1313,6 @@ 0.5 2.0 2 - true Controls when to apply the new estimated airspeed scale(s) @@ -9656,7 +1324,8 @@ Wind estimator true airspeed scale process noise spectral density - Airspeed scale process noise of the internal wind estimator(s) of the airspeed selector. When unaided, the scale uncertainty (1-sigma, unitless) increases by this amount every second. + Airspeed scale process noise of the internal wind estimator(s) of the airspeed selector. +When unaided, the scale uncertainty (1-sigma, unitless) increases by this amount every second. 0 0.1 1/s/sqrt(Hz) @@ -9677,17 +1346,19 @@ m/s 1 - - Horizontal wind uncertainty threshold for synthetic airspeed - The synthetic airspeed estimate (from groundspeed and heading) will be declared valid as soon and as long the horizontal wind uncertainty is below this value. - 0.001 + + Horizontal wind uncertainty threshold for valid ground-minus-wind + The airspeed alternative derived from groundspeed and heading will be declared valid +as soon and as long the horizontal wind uncertainty is below this value. + 0.01 5 m/s - 3 + 2 Wind estimator wind process noise spectral density - Wind process noise of the internal wind estimator(s) of the airspeed selector. When unaided, the wind estimate uncertainty (1-sigma, in m/s) increases by this amount every second. + Wind process noise of the internal wind estimator(s) of the airspeed selector. +When unaided, the wind estimate uncertainty (1-sigma, in m/s) increases by this amount every second. 0 1 m/s^2/sqrt(Hz) @@ -9711,7 +1382,8 @@ External heading usage mode (from Motion capture/Vision) - Set to 1 to use heading estimate from vision. Set to 2 to use heading from motion capture. + Set to 1 to use heading estimate from vision. +Set to 2 to use heading from motion capture. 0 2 @@ -9722,7 +1394,9 @@ Magnetic declination, in degrees - This parameter is not used in normal operation, as the declination is looked up based on the GPS coordinates of the vehicle. + This parameter is not used in normal operation, +as the declination is looked up based on the +GPS coordinates of the vehicle. deg 2 @@ -9757,7 +1431,9 @@ Controls when to apply the new gains - After the auto-tuning sequence is completed, a new set of gains is available and can be applied immediately or after landing. + After the auto-tuning sequence is completed, +a new set of gains is available and can be applied +immediately or after landing. Do not apply the new gains (logging only) Apply the new gains after disarm @@ -9766,7 +1442,11 @@ Tuning axes selection - Defines which axes will be tuned during the auto-tuning sequence Set bits in the following positions to enable: 0 : Roll 1 : Pitch 2 : Yaw + Defines which axes will be tuned during the auto-tuning sequence +Set bits in the following positions to enable: +0 : Roll +1 : Pitch +2 : Yaw 1 7 @@ -9776,8 +1456,8 @@ - Enable/disable auto tuning using an RC AUX input - Defines which RC_MAP_AUXn parameter maps the RC channel used to enable/disable auto tuning. + Enable/disable auto tuning using a manual control AUX input + Defines which RC_MAP_AUXn parameter maps the manual control channel used to enable/disable auto tuning. 0 6 @@ -9790,17 +1470,6 @@ Aux6 - - Start the autotuning sequence - WARNING: this will inject steps to the rate controller and can be dangerous. Only activate if you know what you are doing, and in a safe environment. Any motion of the remote stick will abort the signal injection and reset this parameter Best is to perform the identification in position or hold mode. Increase the amplitude of the injected signal using FW_AT_SYSID_AMP for more signal/noise ratio - - - Amplitude of the injected signal - This parameter scales the signal sent to the rate controller during system identification. - 0.1 - 6.0 - 1 - Start frequency of the injected signal Can be set lower or higher than the end frequency @@ -9809,7 +1478,7 @@ Hz 1 - + End frequency of the injected signal Can be set lower or higher than the start frequency 0.1 @@ -9825,7 +1494,7 @@ s 0 - + Input signal type Type of signal used during system identification to excite the system. @@ -9836,7 +1505,12 @@ Controls when to apply the new gains - After the auto-tuning sequence is completed, a new set of gains is available and can be applied immediately or after landing. WARNING Applying the gains in air is dangerous as there is no guarantee that those new gains will be able to stabilize the drone properly. + After the auto-tuning sequence is completed, +a new set of gains is available and can be applied +immediately or after landing. +WARNING Applying the gains in air is dangerous as there is no +guarantee that those new gains will be able to stabilize +the drone properly. Do not apply the new gains (logging only) Apply the new gains after disarm @@ -9853,10 +1527,6 @@ s 2 - - Start the autotuning sequence - WARNING: this will inject steps to the rate controller and can be dangerous. Only activate if you know what you are doing, and in a safe environment. Any motion of the remote stick will abort the signal injection and reset this parameter Best is to perform the identification in position or hold mode. Increase the amplitude of the injected signal using MC_AT_SYSID_AMP for more signal/noise ratio - Amplitude of the injected signal 0.1 @@ -9865,12 +1535,6 @@ - - Battery 1 current per volt (A/V) - The voltage seen by the ADC multiplied by this factor will determine the battery current. A value of -1 means to use the board default. - 8 - True - Battery 1 capacity Defines the capacity of battery 1 in mAh. @@ -9881,17 +1545,6 @@ 50 True - - Battery 1 Current ADC Channel - This parameter specifies the ADC channel used to monitor current of main power battery. A value of -1 means to use the board default. - True - - - Battery 1 idle current overwrite - This parameter allows to overwrite the current measured during idle (unarmed) state with a user-defined constant value (expressed in amperes). When the system is armed, the measured current is used. This is useful because on certain ESCs current measurements are inaccurate in case of no load. Negative values are ignored and will cause the measured current to be used. The default value of 0 disables the overwrite, in which case the measured value is always used. - 8 - True - Number of cells for battery 1 Defines the number of cells the attached battery consists of. @@ -9927,48 +1580,44 @@ Battery 1 monitoring source - This parameter controls the source of battery data. The value 'Power Module' means that measurements are expected to come from a power module. If the value is set to 'External' then the system expects to receive mavlink battery status messages. If the value is set to 'ESCs', the battery information are taken from the esc_status message. This requires the ESC to provide both voltage as well as current. + This parameter controls the source of battery data. The value 'Power Module / Analog' +means that measurements are expected to come from either analog (ADC) inputs +or an I2C power monitor (e.g. INA226). Analog inputs are voltage and current +measurements read from the board's ADC channels, typically from an onboard +voltage divider and current shunt, or an external analog power module. +I2C power monitors are digital sensors on the I2C bus. +If the value is set to 'External' then the system expects to receive MAVLink +or CAN battery status messages, or the battery data is published by an external driver. +If the value is set to 'ESCs', the battery information are taken from the esc_status message. +This requires the ESC to provide both voltage as well as current (via ESC telemetry). True Disabled - Power Module + Power Module / Analog External ESCs - - Battery 1 Voltage ADC Channel - This parameter specifies the ADC channel used to monitor voltage of main power battery. A value of -1 means to use the board default. - True - Full cell voltage - Defines the voltage where a single cell of the battery is considered full. For a more accurate estimate set this below the nominal voltage of e.g. 4.2V + Defines the voltage where a single cell of the battery is considered full. +For a more accurate estimate set this below the nominal voltage of e.g. 4.2V V 2 0.01 True - - Battery 1 voltage divider (V divider) - This is the divider from battery 1 voltage to ADC voltage. If using e.g. Mauch power modules the value from the datasheet can be applied straight here. A value of -1 means to use the board default. - 8 - True - Empty cell voltage - Defines the voltage where a single cell of the battery is considered empty. The voltage should be chosen above the steep dropoff at 3.5V. A typical lithium battery can only be discharged under high load down to 10% before it drops off to a voltage level damaging the cells. + Defines the voltage where a single cell of the battery is considered empty. +The voltage should be chosen above the steep dropoff at 3.5V. A typical +lithium battery can only be discharged under high load down to 10% before +it drops off to a voltage level damaging the cells. V 2 0.01 True - - Battery 2 current per volt (A/V) - The voltage seen by the ADC multiplied by this factor will determine the battery current. A value of -1 means to use the board default. - 8 - True - Battery 2 capacity Defines the capacity of battery 2 in mAh. @@ -9979,17 +1628,6 @@ 50 True - - Battery 2 Current ADC Channel - This parameter specifies the ADC channel used to monitor current of main power battery. A value of -1 means to use the board default. - True - - - Battery 2 idle current overwrite - This parameter allows to overwrite the current measured during idle (unarmed) state with a user-defined constant value (expressed in amperes). When the system is armed, the measured current is used. This is useful because on certain ESCs current measurements are inaccurate in case of no load. Negative values are ignored and will cause the measured current to be used. The default value of 0 disables the overwrite, in which case the measured value is always used. - 8 - True - Number of cells for battery 2 Defines the number of cells the attached battery consists of. @@ -10025,37 +1663,39 @@ Battery 2 monitoring source - This parameter controls the source of battery data. The value 'Power Module' means that measurements are expected to come from a power module. If the value is set to 'External' then the system expects to receive mavlink battery status messages. If the value is set to 'ESCs', the battery information are taken from the esc_status message. This requires the ESC to provide both voltage as well as current. + This parameter controls the source of battery data. The value 'Power Module / Analog' +means that measurements are expected to come from either analog (ADC) inputs +or an I2C power monitor (e.g. INA226). Analog inputs are voltage and current +measurements read from the board's ADC channels, typically from an onboard +voltage divider and current shunt, or an external analog power module. +I2C power monitors are digital sensors on the I2C bus. +If the value is set to 'External' then the system expects to receive MAVLink +or CAN battery status messages, or the battery data is published by an external driver. +If the value is set to 'ESCs', the battery information are taken from the esc_status message. +This requires the ESC to provide both voltage as well as current (via ESC telemetry). True Disabled - Power Module + Power Module / Analog External ESCs - - Battery 2 Voltage ADC Channel - This parameter specifies the ADC channel used to monitor voltage of main power battery. A value of -1 means to use the board default. - True - Full cell voltage - Defines the voltage where a single cell of the battery is considered full. For a more accurate estimate set this below the nominal voltage of e.g. 4.2V + Defines the voltage where a single cell of the battery is considered full. +For a more accurate estimate set this below the nominal voltage of e.g. 4.2V V 2 0.01 True - - Battery 2 voltage divider (V divider) - This is the divider from battery 2 voltage to ADC voltage. If using e.g. Mauch power modules the value from the datasheet can be applied straight here. A value of -1 means to use the board default. - 8 - True - Empty cell voltage - Defines the voltage where a single cell of the battery is considered empty. The voltage should be chosen above the steep dropoff at 3.5V. A typical lithium battery can only be discharged under high load down to 10% before it drops off to a voltage level damaging the cells. + Defines the voltage where a single cell of the battery is considered empty. +The voltage should be chosen above the steep dropoff at 3.5V. A typical +lithium battery can only be discharged under high load down to 10% before +it drops off to a voltage level damaging the cells. V 2 0.01 @@ -10106,18 +1746,28 @@ Battery 3 monitoring source - This parameter controls the source of battery data. The value 'Power Module' means that measurements are expected to come from a power module. If the value is set to 'External' then the system expects to receive mavlink battery status messages. If the value is set to 'ESCs', the battery information are taken from the esc_status message. This requires the ESC to provide both voltage as well as current. + This parameter controls the source of battery data. The value 'Power Module / Analog' +means that measurements are expected to come from either analog (ADC) inputs +or an I2C power monitor (e.g. INA226). Analog inputs are voltage and current +measurements read from the board's ADC channels, typically from an onboard +voltage divider and current shunt, or an external analog power module. +I2C power monitors are digital sensors on the I2C bus. +If the value is set to 'External' then the system expects to receive MAVLink +or CAN battery status messages, or the battery data is published by an external driver. +If the value is set to 'ESCs', the battery information are taken from the esc_status message. +This requires the ESC to provide both voltage as well as current (via ESC telemetry). True Disabled - Power Module + Power Module / Analog External ESCs Full cell voltage - Defines the voltage where a single cell of the battery is considered full. For a more accurate estimate set this below the nominal voltage of e.g. 4.2V + Defines the voltage where a single cell of the battery is considered full. +For a more accurate estimate set this below the nominal voltage of e.g. 4.2V V 2 0.01 @@ -10125,18 +1775,19 @@ Empty cell voltage - Defines the voltage where a single cell of the battery is considered empty. The voltage should be chosen above the steep dropoff at 3.5V. A typical lithium battery can only be discharged under high load down to 10% before it drops off to a voltage level damaging the cells. + Defines the voltage where a single cell of the battery is considered empty. +The voltage should be chosen above the steep dropoff at 3.5V. A typical +lithium battery can only be discharged under high load down to 10% before +it drops off to a voltage level damaging the cells. V 2 0.01 True - - This parameter is deprecated. Please use BAT1_I_CHANNEL - Expected battery current in flight - This value is used to initialize the in-flight average current estimation, which in turn is used for estimating remaining flight time and RTL triggering. + This value is used to initialize the in-flight average current estimation, +which in turn is used for estimating remaining flight time and RTL triggering. 0 500 A @@ -10145,7 +1796,9 @@ Critical threshold - Sets the threshold when the battery will be reported as critically low. This has to be lower than the low threshold. This threshold commonly will trigger RTL. + Sets the threshold when the battery will be reported as critically low. +This has to be lower than the low threshold. This threshold commonly +will trigger RTL. 0.05 0.5 norm @@ -10154,7 +1807,9 @@ Emergency threshold - Sets the threshold when the battery will be reported as dangerously low. This has to be lower than the critical threshold. This threshold commonly will trigger landing. + Sets the threshold when the battery will be reported as dangerously low. +This has to be lower than the critical threshold. This threshold commonly +will trigger landing. 0.03 0.5 norm @@ -10163,83 +1818,14 @@ Low threshold - Sets the threshold when the battery will be reported as low. This has to be higher than the critical threshold. + Sets the threshold when the battery will be reported as low. +This has to be higher than the critical threshold. 0.12 0.5 norm 2 0.01 - - Offset in volt as seen by the ADC input of the current sensor - This offset will be subtracted before calculating the battery current based on the voltage. - 8 - - - - - Enable USB autostart - true - - Disabled - Auto-detect - MAVLink - - - - Specify USB MAVLink mode - true - - normal - custom - onboard - osd - magic - config - iridium - minimal - extvision - extvisionmin - gimbal - onboard_low_bandwidth - uavionix - - - - - - Camera strobe delay - This parameter sets the delay between image integration start and strobe firing - 0.0 - 100.0 - ms - 1 - - - - - Camera capture edge - true - - Falling edge - Rising edge - - - - Camera capture feedback - Enables camera capture feedback - true - - - Camera capture timestamping mode - Change time measurement - true - - Get absolute timestamp - Get timestamp of mid exposure (active high) - Get timestamp of mid exposure (active low) - - @@ -10282,7 +1868,8 @@ Minimum camera trigger interval - This parameter sets the minimum time between two consecutive trigger events the specific camera setup is supporting. + This parameter sets the minimum time between two consecutive trigger events +the specific camera setup is supporting. 1.0 10000.0 ms @@ -10331,39 +1918,54 @@ Circuit breaker for disabling buzzer - Setting this parameter to 782097 will disable the buzzer audio notification. Setting this parameter to 782090 will disable the startup tune, while keeping all others enabled. + Setting this parameter to 782097 will disable the buzzer audio notification. +Setting this parameter to 782090 will disable the startup tune, while keeping +all others enabled. 0 782097 true Circuit breaker for flight termination - Setting this parameter to 121212 will disable the flight termination action if triggered by the FailureDetector logic or if FMU is lost. This circuit breaker does not affect the RC loss, data link loss, geofence, and takeoff failure detection safety logic. + Setting this parameter to 121212 will disable the flight termination action if triggered +by the FailureDetector logic or if FMU is lost. +This circuit breaker does not affect the RC loss, data link loss, geofence, +and takeoff failure detection safety logic. 0 121212 true Circuit breaker for IO safety - Setting this parameter to 22027 will disable IO safety. WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK + Setting this parameter to 22027 will disable IO safety. +WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK 0 22027 Circuit breaker for power supply check - Setting this parameter to 894281 will disable the power valid checks in the commander. WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK + Setting this parameter to 894281 will disable the power valid +checks in the commander. +WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK 0 894281 Circuit breaker for USB link check - Setting this parameter to 197848 will disable the USB connected checks in the commander, setting it to 0 keeps them enabled (recommended). We are generally recommending to not fly with the USB link connected and production vehicles should set this parameter to zero to prevent users from flying USB powered. However, for R&D purposes it has proven over the years to work just fine. + Setting this parameter to 197848 will disable the USB connected +checks in the commander, setting it to 0 keeps them enabled (recommended). +We are generally recommending to not fly with the USB link +connected and production vehicles should set this parameter to +zero to prevent users from flying USB powered. However, for R&D purposes +it has proven over the years to work just fine. 0 197848 Circuit breaker for arming in fixed-wing mode check - Setting this parameter to 159753 will enable arming in fixed-wing mode for VTOLs. WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK + Setting this parameter to 159753 will enable arming in fixed-wing +mode for VTOLs. +WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK 0 159753 @@ -10371,7 +1973,8 @@ Set the actuator failure failsafe mode - Note: actuator failure needs to be enabled and configured via FD_ACT_* parameters. + Note: actuator failure needs to be enabled and configured via FD_ACT_* +parameters. 0 3 @@ -10396,7 +1999,11 @@ Arm authorization method - Methods: - one arm: request authorization and arm when authorization is received - two step arm: 1st arm command request an authorization and 2nd arm command arm the drone if authorized Used if arm authorization is requested by COM_ARM_AUTH_REQ. + Methods: +- one arm: request authorization and arm when authorization is received +- two step arm: 1st arm command request an authorization and +2nd arm command arm the drone if authorized +Used if arm authorization is requested by COM_ARM_AUTH_REQ. one arm two step arm @@ -10408,14 +2015,16 @@ Arm authorization timeout - Timeout for authorizer answer. Used if arm authorization is requested by COM_ARM_AUTH_REQ. + Timeout for authorizer answer. +Used if arm authorization is requested by COM_ARM_AUTH_REQ. s 1 0.1 Minimum battery level for arming - Threshold for battery percentage below arming is prohibited. A negative value means BAT_CRIT_THR is the threshold. + Threshold for battery percentage below arming is prohibited. +A negative value means BAT_CRIT_THR is the threshold. -1 0.9 norm @@ -10424,11 +2033,13 @@ Enable checks on ESCs that report telemetry - If this parameter is set, the system will check ESC's online status and failures. This param is specific for ESCs reporting status. It shall be used only if ESCs support telemetry. + If this parameter is set, the system will check ESC's online status and failures. +This param is specific for ESCs reporting status. It shall be used only if ESCs support telemetry. - Enable FMU SD card hardfault detection check - This check detects if there are hardfault files present on the SD card. If so, and the parameter is enabled, arming is prevented. + Enable FMU SD card hardfault / watchdog detection check + This check detects if there are hardfault / watchdog files present on the +SD card. If so, and the parameter is enabled, arming is prevented. Maximum accelerometer inconsistency between IMU units that will allow arming @@ -10455,7 +2066,8 @@ Enable mag strength preflight check - Check if the estimator detects a strong magnetic disturbance (check enabled by EKF2_MAG_CHECK) + Check if the estimator detects a strong magnetic +disturbance (check enabled by EKF2_MAG_CHECK) Disabled Deny arming @@ -10468,7 +2080,9 @@ Enable Drone ID system detection and health check - This check detects if the Open Drone ID system is missing. Depending on the value of the parameter, the check can be disabled, warn only or deny arming. + This check detects if the Open Drone ID system is missing. +Depending on the value of the parameter, the check can be +disabled, warn only or deny arming. Disabled Warning only @@ -10477,7 +2091,9 @@ Enable FMU SD card detection check - This check detects if the FMU SD card is missing. Depending on the value of the parameter, the check can be disabled, warn only or deny arming. + This check detects if the FMU SD card is missing. +Depending on the value of the parameter, the check can be +disabled, warn only or deny arming. Disabled Warning only @@ -10486,20 +2102,36 @@ Arm switch is a momentary button - 0: Arming/disarming triggers on switch transition. 1: Arming/disarming triggers when holding the momentary button down for COM_RC_ARM_HYST like the stick gesture. + 0: Arming/disarming triggers on switch transition. +1: Arming/disarming triggers when holding the momentary button down like the stick gesture. + + + Enable Traffic Avoidance system detection check + This check detects if a traffic avoidance system (ADSB/FLARM transponder) +is missing. Depending on the value of the parameter, the check can be +disabled, warn only, or deny arming. + + Disabled + Warning only + Enforce for all modes + Enforce for mission modes only + - GPS preflight check - Measures taken when a check defined by EKF2_GPS_CHECK is failing. + Arming without GNSS configuration + Configures whether arming is allowed without GNSS, for modes that require a global position +(specifically, in those modes when a check defined by EKF2_GPS_CHECK fails). +The settings deny arming and warn, allow arming and warn, or silently allow arming. Deny arming - Warning only - Disabled + Allow arming (with warning) + Allow arming (no warning) Maximum allowed CPU load to still arm - The check fails if the CPU load is above this threshold for 2s. A negative value disables the check. + The check fails if the CPU load is above this threshold for 2s. +A negative value disables the check. -1 100 % @@ -10507,22 +2139,44 @@ Time-out for auto disarm after landing - A non-zero, positive value specifies the time-out period in seconds after which the vehicle will be automatically disarmed in case a landing situation has been detected during this period. A zero or negative value means that automatic disarming triggered by landing detection is disabled. + A non-zero, positive value specifies the time-out period in seconds after which the vehicle will be +automatically disarmed in case a landing situation has been detected during this period. +A zero or negative value means that automatic disarming triggered by landing detection is disabled. s 1 0.1 Allow disarming via switch/stick/button on multicopters in manual thrust modes - 0: Disallow disarming when not landed 1: Allow disarming in multicopter flight in modes where the thrust is directly controlled by the throttle stick e.g. Stabilized, Acro + 0: Disallow disarming when not landed +1: Allow disarming in multicopter flight in modes where +the thrust is directly controlled by thr throttle stick +e.g. Stabilized, Acro Time-out for auto disarm if not taking off - A non-zero, positive value specifies the time in seconds, within which the vehicle is expected to take off after arming. In case the vehicle didn't takeoff within the timeout it disarms again. A negative value disables autmoatic disarming triggered by a pre-takeoff timeout. + A non-zero, positive value specifies the time in seconds, within which the +vehicle is expected to take off after arming. In case the vehicle didn't takeoff +within the timeout it disarms again. +A negative value disables autmoatic disarming triggered by a pre-takeoff timeout. s 1 0.1 + + Datalink loss exceptions + Specify modes in which ground control station connection loss is ignored and no failsafe action is triggered. +See also COM_RCL_EXCEPT. + 0 + 31 + + Mission + Auto modes + Offboard + External Mode + Altitude Cruise + + GCS connection loss time threshold After this amount of seconds without datalink, the GCS connection lost mode triggers @@ -10534,7 +2188,11 @@ Delay between failsafe condition triggered and failsafe reaction - Before entering failsafe (RTL, Land, Hold), wait COM_FAIL_ACT_T seconds in Hold mode for the user to realize. During that time the user cannot take over control via the stick override feature (see COM_RC_OVERRIDE). Afterwards the configured failsafe action is triggered and the user may use stick override. A zero value disables the delay and the user cannot take over via stick movements (switching modes is still allowed). + Before entering failsafe (RTL, Land, Hold), wait COM_FAIL_ACT_T seconds in Hold mode +for the user to realize. +During that time the user can switch modes, but cannot take over control via the stick override feature (see COM_RC_OVERRIDE). +Afterwards the configured failsafe action is triggered and the user may use stick override. +A zero value disables the delay. 0.0 25.0 s @@ -10542,12 +2200,15 @@ Next flight UUID - This number is incremented automatically after every flight on disarming in order to remember the next flight UUID. The first flight is 0. + This number is incremented automatically after every flight on +disarming in order to remember the next flight UUID. +The first flight is 0. 0 Mode slot 1 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10564,6 +2225,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10576,7 +2238,8 @@ Mode slot 2 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10593,6 +2256,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10605,7 +2269,8 @@ Mode slot 3 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10622,6 +2287,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10634,7 +2300,8 @@ Mode slot 4 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10651,6 +2318,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10663,7 +2331,8 @@ Mode slot 5 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10680,6 +2349,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10692,7 +2362,8 @@ Mode slot 6 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10709,6 +2380,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10719,9 +2391,10 @@ External Mode 8 - + Remaining flight time low failsafe - Action the system takes when the remaining flight time is below the estimated time it takes to reach the RTL destination. + Action the system takes when the remaining flight time is below +the estimated time it takes to reach the RTL destination. 1 None @@ -10731,7 +2404,9 @@ User Flight Profile - Describes the intended use of the vehicle. Can be used by ground control software or log post processing. This param does not influence the behavior within the firmware. This means for example the control logic is independent of the setting of this param (but depends on other params). + Describes the intended use of the vehicle. +Can be used by ground control software or log post processing. +This param does not influence the behavior within the firmware. This means for example the control logic is independent of the setting of this param (but depends on other params). Default Pro User @@ -10741,7 +2416,13 @@ Maximum allowed flight time - The vehicle aborts the current operation and returns to launch when the time since takeoff is above this value. It is not possible to resume the mission or switch to any auto mode other than RTL or Land. Taking over in any manual mode is still possible. Starting from 90% of the maximum flight time, a warning message will be sent every 1 minute with the remaining time until automatic RTL. Set to -1 to disable. + The vehicle aborts the current operation and returns to launch when +the time since takeoff is above this value. It is not possible to resume the +mission or switch to any auto mode other than RTL or Land. Taking over in any manual +mode is still possible. +Starting from 90% of the maximum flight time, a warning message will be sent +every 1 minute with the remaining time until automatic RTL. +Set to -1 to disable. -1 s @@ -10758,23 +2439,30 @@ High Latency Datalink regain time threshold - After a data link loss: after this number of seconds with a healthy datalink the 'datalink loss' flag is set back to false + After a data link loss: after this number of seconds with a healthy datalink the 'datalink loss' +flag is set back to false 0 60 s Home position enabled - Set home position automatically if possible. + Set home position automatically if possible. +During missions, the latitude/longitude of the home position is locked and will not reset during intermediate landings. +It will only update once the mission is complete or landed outside of a mission. +However, the altitude is still being adjusted to correct for GNSS vertical drift in the first 2 minutes after takeoff. true Allows setting the home position after takeoff - If set to true, the autopilot is allowed to set its home position after takeoff The true home position is back-computed if a local position is estimate if available. If no local position is available, home is set to the current position. + If set to true, the autopilot is allowed to set its home position after takeoff +The true home position is back-computed if a local position is estimate if available. +If no local position is available, home is set to the current position. Imbalanced propeller failsafe mode - Action the system takes when an imbalanced propeller is detected by the failure detector. See also FD_IMB_PROP_THR to set the failure threshold. + Action the system takes when an imbalanced propeller is detected by the failure detector. +See also FD_IMB_PROP_THR to set the failure threshold. 1 Disabled @@ -10785,6 +2473,7 @@ Timeout value for disarming when kill switch is engaged + Use RC_MAP_KILL_SW to map a kill switch. 0.0 30.0 s @@ -10792,7 +2481,10 @@ Timeout for detecting a failure after takeoff - A non-zero, positive value specifies the timeframe in seconds within failure detector is allowed to disarm the vehicle if attitude exceeds the limits defined in FD_FAIL_P and FD_FAIL_R. The check is not executed for flight modes that do support acrobatic maneuvers, e.g: Acro (MC/FW) and Manual (FW). A zero or negative value means that the check is disabled. + A non-zero, positive value specifies the timeframe in seconds within failure detector is allowed to disarm the vehicle +if attitude exceeds the limits defined in FD_FAIL_P and FD_FAIL_R. +The check is not executed for flight modes that do support acrobatic maneuvers, e.g: Acro (MC/FW) and Manual (FW). +A zero or negative value means that the check is disabled. -1.0 5.0 s @@ -10800,7 +2492,8 @@ Battery failsafe mode - Action the system takes at critical battery. See also BAT_CRIT_THR and BAT_EMERGEN_THR for definition of battery states. + Action the system takes at critical battery. See also BAT_CRIT_THR and BAT_EMERGEN_THR +for definition of battery states. Warning Land mode @@ -10809,39 +2502,60 @@ External mode identifier 0 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 1 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 2 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 3 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 4 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 5 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 6 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 7 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. + + + Allow external mode registration while armed + By default disabled for safety reasons Enable Actuator Testing - If set, enables the actuator test interface via MAVLink (ACTUATOR_TEST), that allows spinning the motors and moving the servos for testing purposes. + If set, enables the actuator test interface via MAVLink (ACTUATOR_TEST), that +allows spinning the motors and moving the servos for testing purposes. Time-out to wait when onboard computer connection is lost before warning about loss connection @@ -10852,7 +2566,8 @@ Set offboard loss failsafe mode - The offboard loss failsafe will only be entered after a timeout, set by COM_OF_LOSS_T in seconds. + The offboard loss failsafe will only be entered after a timeout, +set by COM_OF_LOSS_T in seconds. Position mode Altitude mode @@ -10873,19 +2588,16 @@ 0.01 - Expect and require a healthy MAVLink parachute system - - - Position mode navigation loss response - This sets the flight mode that will be used if navigation accuracy is no longer adequate for position control in manual Position mode. - - Altitude mode - Land mode (descend) - + Require MAVLink parachute system to be present and healthy - Horizontal position error threshold - This is the horizontal position error (EPH) threshold that will trigger a failsafe. The default is appropriate for a multicopter. Can be increased for a fixed-wing. If the previous position error was below this threshold, there is an additional factor of 2.5 applied (threshold for invalidation 2.5 times the one for validation). Set to -1 to disable. + Horizontal position error threshold for hovering systems + This is the horizontal position error (EPH) threshold that will trigger a failsafe. +If the previous position error was below this threshold, there is an additional +factor of 2.5 applied (threshold for invalidation 2.5 times the one for validation). +Only used for multicopters and VTOLs in hover mode. +Independent from estimator positioning data timeout threshold (see EKF2_NOAID_TOUT). +Set to -1 to disable. -1 400 m @@ -10893,7 +2605,10 @@ Low position accuracy action - Action the system takes when the estimated position has an accuracy below the specified threshold. See COM_POS_LOW_EPH to set the failsafe threshold. The failsafe action is only executed if the vehicle is in auto mission or auto loiter mode, otherwise it is only a warning. + Action the system takes when the estimated position has an accuracy below the specified threshold. +See COM_POS_LOW_EPH to set the failsafe threshold. +The failsafe action is only executed if the vehicle is in auto mission or auto loiter mode, +otherwise it is only a warning. 1 None @@ -10906,20 +2621,25 @@ Low position accuracy failsafe threshold - This triggers the action specified in COM_POS_LOW_ACT if the estimated position accuracy is below this threshold. Local position has to be still declared valid, which requires some kind of velocity aiding or large dead-reckoning time (EKF2_NOAID_TOUT), and a high failsafe threshold (COM_POS_FS_EPH). Set to -1 to disable. + This triggers the action specified in COM_POS_LOW_ACT if the estimated position accuracy is below this threshold. +Local position has to be still declared valid, which requires some kind of velocity aiding or large dead-reckoning time (EKF2_NOAID_TOUT), +and a high failsafe threshold (COM_POS_FS_EPH). +Set to -1 to disable. -1 1000 m Required number of redundant power modules - This configures a check to verify the expected number of 5V rail power supplies are present. By default only one is expected. Note: CBRK_SUPPLY_CHK disables all power checks including this one. + This configures a check to verify the expected number of 5V rail power supplies are present. By default only one is expected. +Note: CBRK_SUPPLY_CHK disables all power checks including this one. 0 4 Condition to enter prearmed mode - Condition to enter the prearmed state, an intermediate state between disarmed and armed in which non-throttling actuators are active. + Condition to enter the prearmed state, an intermediate state between disarmed and armed +in which non-throttling actuators are active. Disabled Safety button @@ -10927,7 +2647,7 @@ - Set command after a quadchute + Set action after a quadchute Warning only Return mode @@ -10937,46 +2657,61 @@ Maximum allowed RAM usage to pass checks - The check fails if the RAM usage is above this threshold. A negative value disables the check. + The check fails if the RAM usage is above this threshold. +A negative value disables the check. -1 100 % 1 - RC loss exceptions - Specify modes in which RC loss is ignored and the failsafe action not triggered. + Manual control loss exceptions + Specify modes in which stick input is ignored and no failsafe action is triggered. +External modes requiring stick input will still failsafe. +Auto modes are: Hold, Takeoff, Land, RTL, Descend, Follow Target, Precland, Orbit. 0 31 Mission - Hold + Auto modes Offboard + External Mode + Altitude Cruise - - RC input arm/disarm command duration - The default value of 1000 requires the stick to be held in the arm or disarm position for 1 second. - 100 - 1500 - ms - - RC control input mode - A value of 0 enables RC transmitter control (only). A valid RC transmitter calibration is required. A value of 1 allows joystick control only. RC input handling and the associated checks are disabled. A value of 2 allows either RC Transmitter or Joystick input. The first valid input is used, will fallback to other sources if the input stream becomes invalid. A value of 3 allows either input from RC or joystick. The first available source is selected and used until reboot. A value of 4 ignores any stick input. + Manual control input source configuration + Selects stick input selection behavior: +either a traditional remote control receiver (RC) or a MAVLink joystick (MANUAL_CONTROL message) +Priority sources are immediately switched to whenever they get valid. +0 RC only. Requires valid RC calibration. +1 MAVLink only. RC and related checks are disabled. +2 Switches only if current source becomes invalid. +3 Locks to the first valid source until reboot. +4 Ignores all sources. +5 RC priority, then MAVLink (lower instance before higher) +6 MAVLink priority (lower instance before higher), then RC +7 RC priority, then MAVLink (higher instance before lower) +8 MAVLink priority (higher instance before lower), then RC 0 - 4 + 8 - RC Transmitter only - Joystick only - RC and Joystick with fallback - RC or Joystick keep first - Stick input disabled + RC only + MAVLink only + RC or MAVLink with fallback + RC or MAVLink keep first + Disable manual control + Prio: RC > MAVL 1 > MAVL 2 + Prio: MAVL 1 > MAVL 2 > RC + Prio: RC > MAVL 2 > MAVL 1 + Prio: MAVL 2 > MAVL 1 > RC Manual control loss timeout - The time in seconds without a new setpoint from RC or Joystick, after which the connection is considered lost. This must be kept short as the vehicle will use the last supplied setpoint until the timeout triggers. + The time in seconds without a new setpoint from RC or Joystick, after which the connection is considered lost. +This must be kept short as the vehicle will use the last supplied setpoint until the timeout triggers. +Ensure the value is not set lower than the update interval of the RC or Joystick. 0 35 s @@ -10984,8 +2719,11 @@ 0.1 - Enable RC stick override of auto and/or offboard modes - When RC stick override is enabled, moving the RC sticks more than COM_RC_STICK_OV immediately gives control back to the pilot by switching to Position mode and if position is unavailable Altitude mode. Note: Only has an effect on multicopters, and VTOLs in multicopter mode. + Enable manual control stick override + When enabled, moving the sticks more than COM_RC_STICK_OV +immediately gives control back to the pilot by switching to Position mode and +if position is unavailable Altitude mode. +Note: Only has an effect on multicopters, and VTOLs in multicopter mode. 0 3 @@ -10994,8 +2732,9 @@ - RC stick override threshold - If COM_RC_OVERRIDE is enabled and the joystick input is moved more than this threshold the autopilot the pilot takes over control. + Stick override threshold + If COM_RC_OVERRIDE is enabled and the joystick input is moved more than this threshold +the autopilot the pilot takes over control. 5 80 % @@ -11004,7 +2743,11 @@ Enforced delay between arming and further navigation - The minimal time from arming the motors until moving the vehicle is possible is COM_SPOOLUP_TIME seconds. Goal: - Motors and propellers spool up to idle speed before getting commanded to spin faster - Timeout for ESCs and smart batteries to successfulyy do failure checks e.g. for stuck rotors before the vehicle is off the ground + The minimal time from arming the motors until moving the vehicle is possible is COM_SPOOLUP_TIME seconds. +Goal: +- Motors and propellers spool up to idle speed before getting commanded to spin faster +- Timeout for ESCs and smart batteries to successfulyy do failure checks +e.g. for stuck rotors before the vehicle is off the ground 0 30 s @@ -11025,7 +2768,10 @@ Minimum speed for the throw start - When the throw launch is enabled, the drone will only allow motors to spin after this speed is exceeded before detecting the freefall. This is a safety feature to ensure the drone does not turn on after accidental drop or a rapid movement before the throw. Set to 0 to disable. + When the throw launch is enabled, the drone will only allow motors to spin after this speed +is exceeded before detecting the freefall. This is a safety feature to ensure the drone does +not turn on after accidental drop or a rapid movement before the throw. +Set to 0 to disable. 0 m/s 1 @@ -11033,14 +2779,18 @@ Horizontal velocity error threshold - This is the horizontal velocity error (EVH) threshold that will trigger a failsafe. The default is appropriate for a multicopter. Can be increased for a fixed-wing. If the previous velocity error was below this threshold, there is an additional factor of 2.5 applied (threshold for invalidation 2.5 times the one for validation). + This is the horizontal velocity error (EVH) threshold that will trigger a failsafe. +The default is appropriate for a multicopter. Can be increased for a fixed-wing. +If the previous velocity error was below this threshold, there is an additional +factor of 2.5 applied (threshold for invalidation 2.5 times the one for validation). 0 m/s 1 High wind speed failsafe threshold - Wind speed threshold above which an automatic failsafe action is triggered. Failsafe action can be specified with COM_WIND_MAX_ACT. + Wind speed threshold above which an automatic failsafe action is triggered. +Failsafe action can be specified with COM_WIND_MAX_ACT. -1 m/s 1 @@ -11048,7 +2798,11 @@ High wind failsafe mode - Action the system takes when a wind speed above the specified threshold is detected. See COM_WIND_MAX to set the failsafe threshold. If enabled, it is not possible to resume the mission or switch to any auto mode other than RTL or Land if this threshold is exceeded. Taking over in any manual mode is still possible. + Action the system takes when a wind speed above the specified threshold is detected. +See COM_WIND_MAX to set the failsafe threshold. +If enabled, it is not possible to resume the mission or switch to any auto mode other than +RTL or Land if this threshold is exceeded. Taking over in any manual +mode is still possible. 1 None @@ -11061,7 +2815,9 @@ Wind speed warning threshold - A warning is triggered if the currently estimated wind speed is above this value. Warning is sent periodically (every 1 minute). Set to -1 to disable. + A warning is triggered if the currently estimated wind speed is above this value. +Warning is sent periodically (every 1 minute). +Set to -1 to disable. -1 m/s 1 @@ -11069,7 +2825,9 @@ Set GCS connection loss failsafe mode - The GCS connection loss failsafe will only be entered after a timeout, set by COM_DL_LOSS_T in seconds. Once the timeout occurs the selected action will be executed. + The GCS connection loss failsafe will only be entered after a timeout, +set by COM_DL_LOSS_T in seconds. Once the timeout occurs the selected +action will be executed. 0 6 @@ -11082,8 +2840,9 @@ - Set RC loss failsafe mode - The RC loss failsafe will only be entered after a timeout, set by COM_RC_LOSS_T in seconds. If RC input checks have been disabled by setting the COM_RC_IN_MODE param it will not be triggered. + Set manual control loss failsafe mode + The manual control loss failsafe will only be entered after a timeout, +set by COM_RC_LOSS_T in seconds. 1 6 @@ -11095,192 +2854,6 @@ - - - UAVCAN/CAN v1 bus bitrate - 20000 - 1000000 - bit/s - true - - - Cyphal - 0 - Cyphal disabled. 1 - Enables Cyphal - true - - - Cyphal Node ID - Read the specs at http://uavcan.org to learn more about Node ID. - -1 - 125 - true - - - actuator_outputs uORB over Cyphal publication port ID - -1 - 6143 - - - UDRAL battery parameters subscription port ID - -1 - 6143 - - - UDRAL battery status subscription port ID - -1 - 6143 - - - UDRAL battery energy source subscription port ID - -1 - 6143 - - - ESC 0 subscription port ID - -1 - 6143 - - - Cyphal ESC publication port ID - -1 - 6143 - - - Cyphal ESC 0 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 1 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 2 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 3 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 4 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 5 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 6 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 7 zubax feedback port ID - -1 - 6143 - - - GPS 0 subscription port ID - -1 - 6143 - - - GPS 1 subscription port ID - -1 - 6143 - - - Cyphal GPS publication port ID - -1 - 6143 - - - Cyphal legacy battery port ID - -1 - 6143 - - - Cyphal ESC readiness port ID - -1 - 6143 - - - Cyphal Servo publication port ID - -1 - 6143 - - - sensor_gps uORB over Cyphal subscription port ID - -1 - 6143 - - - sensor_gps uORB over Cyphal publication port ID - -1 - 6143 - - - - - DSHOT 3D deadband high - When the actuator_output is between DSHOT_3D_DEAD_L and DSHOT_3D_DEAD_H, motor will not spin. This value is with respect to the mixer_module range (0-1999), not the DSHOT values. - 1000 - 1999 - - - DSHOT 3D deadband low - When the actuator_output is between DSHOT_3D_DEAD_L and DSHOT_3D_DEAD_H, motor will not spin. This value is with respect to the mixer_module range (0-1999), not the DSHOT values. - 0 - 1000 - - - Allows for 3d mode when using DShot and suitable mixer - WARNING: ESC must be configured for 3D mode, and DSHOT_MIN set to 0. This splits the throttle ranges in two. Direction 1) 48 is the slowest, 1047 is the fastest. Direction 2) 1049 is the slowest, 2047 is the fastest. When mixer outputs 1000 or value inside DSHOT 3D deadband, DShot 0 is sent. - - - Enable bidirectional DShot - This parameter enables bidirectional DShot which provides RPM feedback. Note that this requires ESCs that support bidirectional DSHot, e.g. BlHeli32. This is not the same as DShot telemetry which requires an additional serial connection. - True - - - Minimum DShot Motor Output - Minimum Output Value for DShot in percent. The value depends on the ESC. Make sure to set this high enough so that the motors are always spinning while armed. - 0 - 1 - % - 2 - 0.01 - - - Serial Configuration for DShot Driver - Configure on which serial port to run DShot Driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Number of magnetic poles of the motors - Specify the number of magnetic poles of the motors. It is required to compute the RPM value from the eRPM returned with the ESC telemetry. Either get the number from the motor spec sheet or count the magnets on the bell of the motor (not the stator magnets). Typical motors for 5 inch props have 14 poles. - - 1-sigma IMU accelerometer switch-on bias @@ -11314,30 +2887,168 @@ m/s^2 2 - - Accel bias learning inhibit time constant - The vector magnitude of angular rate and acceleration used to check if learning should be inhibited has a peak hold filter applied to it with an exponential decay. This parameter controls the time constant of the decay. - 0.1 - 1.0 - s - 2 + + Accel bias learning inhibit time constant + The vector magnitude of angular rate and acceleration used to check if learning should be inhibited has a peak hold filter applied to it with an exponential decay. This parameter controls the time constant of the decay. + 0.1 + 1.0 + s + 2 + + + Process noise for IMU accelerometer bias prediction + 0.0 + 0.01 + m/s^3 + 6 + + + Accelerometer noise for covariance prediction + 0.01 + 1.0 + m/s^2 + 2 + + + Auxiliary global position sensor 0 aiding + Set bits in the following positions to enable: 0 : Horizontal position fusion 1 : Vertical position fusion + 0 + 3 + + Horizontal position + Vertical position + + + + Auxiliary global position sensor 0 delay (to IMU) + 0 + 1000 + ms + 1 + True + + + Gate size for auxiliary global position sensor 0 fusion + Sets the number of standard deviations used by the innovation consistency test. + 1.0 + SD + 1 + + + Auxiliary global position sensor 0 ID + Sensor ID for slot 0. Set to 0 to disable this slot. + 0 + 255 + + + Fusion reset mode for sensor 0 + Automatic: reset on fusion timeout if no other source of position is available Dead-reckoning: reset on fusion timeout if no source of velocity is available + + Automatic + Dead-reckoning + + + + Measurement noise for auxiliary global position sensor 0 + Used to lower bound or replace the uncertainty included in the message + 0.01 + m + 2 + + + Auxiliary global position sensor 1 aiding + Set bits in the following positions to enable: 0 : Horizontal position fusion 1 : Vertical position fusion + 0 + 3 + + Horizontal position + Vertical position + + + + Auxiliary global position sensor 1 delay (to IMU) + 0 + 1000 + ms + 1 + True + + + Gate size for auxiliary global position sensor 1 fusion + Sets the number of standard deviations used by the innovation consistency test. + 1.0 + SD + 1 + + + Auxiliary global position sensor 1 ID + Sensor ID for slot 1. Set to 0 to disable this slot. + 0 + 255 + + + Fusion reset mode for sensor 1 + Automatic: reset on fusion timeout if no other source of position is available Dead-reckoning: reset on fusion timeout if no source of velocity is available + + Automatic + Dead-reckoning + + + + Measurement noise for auxiliary global position sensor 1 + Used to lower bound or replace the uncertainty included in the message + 0.01 + m + 2 + + + Auxiliary global position sensor 2 aiding + Set bits in the following positions to enable: 0 : Horizontal position fusion 1 : Vertical position fusion + 0 + 3 + + Horizontal position + Vertical position + + + + Auxiliary global position sensor 2 delay (to IMU) + 0 + 1000 + ms + 1 + True + + + Gate size for auxiliary global position sensor 2 fusion + Sets the number of standard deviations used by the innovation consistency test. + 1.0 + SD + 1 - - Process noise for IMU accelerometer bias prediction - 0.0 - 0.01 - m/s^3 - 6 + + Auxiliary global position sensor 2 ID + Sensor ID for slot 2. Set to 0 to disable this slot. + 0 + 255 - - Accelerometer noise for covariance prediction + + Fusion reset mode for sensor 2 + Automatic: reset on fusion timeout if no other source of position is available Dead-reckoning: reset on fusion timeout if no source of velocity is available + + Automatic + Dead-reckoning + + + + Measurement noise for auxiliary global position sensor 2 + Used to lower bound or replace the uncertainty included in the message 0.01 - 1.0 - m/s^2 + m 2 - - Aux global position (AGP) sensor aiding + + Auxiliary global position sensor 3 aiding Set bits in the following positions to enable: 0 : Horizontal position fusion 1 : Vertical position fusion 0 3 @@ -11346,23 +3057,37 @@ Vertical position - - Aux global position estimator delay relative to IMU measurements + + Auxiliary global position sensor 3 delay (to IMU) 0 - 300 + 1000 ms 1 True - - Gate size for aux global position fusion + + Gate size for auxiliary global position sensor 3 fusion Sets the number of standard deviations used by the innovation consistency test. 1.0 SD 1 - - Measurement noise for aux global position measurements + + Auxiliary global position sensor 3 ID + Sensor ID for slot 3. Set to 0 to disable this slot. + 0 + 255 + + + Fusion reset mode for sensor 3 + Automatic: reset on fusion timeout if no other source of position is available Dead-reckoning: reset on fusion timeout if no source of velocity is available + + Automatic + Dead-reckoning + + + + Measurement noise for auxiliary global position sensor 3 Used to lower bound or replace the uncertainty included in the message 0.01 m @@ -11504,6 +3229,10 @@ EKF2 enable + + Enable constant position fusion during engine warmup + When enabled, constant position fusion is enabled when the vehicle is landed and armed. This is intended for IC engine warmup (e.g., fuel engines on catapult) to allow mode transitions to auto/takeoff despite vibrations from running engines. + Measurement noise for vision angle measurements Used to lower bound or replace the uncertainty included in the message @@ -11594,7 +3323,7 @@ Enable synthetic sideslip fusion - For reliable wind estimation both sideslip and airspeed fusion (see EKF2_ARSP_THR) should be enabled. Only applies to fixed-wing vehicles (or VTOLs in fixed-wing mode). Note: side slip fusion is currently not supported for tailsitters. + For reliable wind estimation both sideslip and airspeed fusion (see EKF2_ARSP_THR) should be enabled. Only applies to vehicles in fixed-wing mode or with airspeed fusion active. Note: side slip fusion is currently not supported for tailsitters. 1-sigma IMU gyro switch-on bias @@ -11620,11 +3349,11 @@ m 1 - + Integer bitmask controlling GPS checks Each threshold value is defined by the parameter indicated next to the check. Drift and offset checks only run when the vehicle is on ground and stationary. 0 - 1023 + 4095 Sat count (EKF2_REQ_NSATS) PDOP (EKF2_REQ_PDOP) @@ -11636,6 +3365,8 @@ Horizontal speed offset (EKF2_REQ_HDRIFT) Vertical speed offset (EKF2_REQ_VDRIFT) Spoofing + GPS fix type (EKF2_REQ_FIX) + Jamming @@ -11651,28 +3382,37 @@ - GPS measurement delay relative to IMU measurements + GPS measurement delay relative to IMU measurement + GPS measurement delay relative to IMU measurement if PPS time correction is not available/enabled (PPS_CAP_ENABLE). 0 300 ms 1 True + + Fusion reset mode + Automatic: reset on fusion timeout if no other source of position is available. Dead-reckoning: reset on fusion timeout if no source of velocity is available. + + Automatic + Dead-reckoning + + X position of GPS antenna in body frame - Forward axis with origin relative to vehicle centre of gravity + Forward (roll) axis with origin relative to vehicle centre of gravity m 3 Y position of GPS antenna in body frame - Forward axis with origin relative to vehicle centre of gravity + Right (pitch) axis with origin relative to vehicle centre of gravity m 3 Z position of GPS antenna in body frame - Forward axis with origin relative to vehicle centre of gravity + Down (yaw) axis with origin relative to vehicle centre of gravity m 3 @@ -11899,7 +3639,7 @@ 1/s 2 - + Expected range finder reading when on ground If the vehicle is on ground, is not moving as determined by the motion test and the range finder is returning invalid or no data, then an assumed range value of EKF2_MIN_RNG will be used by the terrain estimator so that a terrain height estimate is available at the start of flight in situations where the range finder may be inside its minimum measurements distance when on ground. 0.01 @@ -12062,6 +3802,19 @@ m 1 + + Required GPS fix + Minimum GPS fix type required for GPS usage. + + No fix required + 2D fix + 3D fix + RTCM code differential + RTK float + RTK fixed + Extrapolated + + Required GPS health time on startup Minimum continuous period without GPS failure required to mark a healthy GPS status. It can be reduced to speed up initialization, but it's recommended to keep this unchanged for a vehicle. @@ -12109,13 +3862,6 @@ 10.0 m - - Gate size used for innovation consistency checks for range aid fusion - A lower value means HAGL needs to be more stable in order to use range finder for height estimation in range aid mode - 0.1 - 5.0 - SD - Maximum horizontal velocity allowed for conditional range aid mode If the vehicle horizontal speed exceeds this value then the estimator will not fuse range measurements to estimate its height. This only applies when conditional range aid mode is activated (EKF2_RNG_CTRL = 1). @@ -12140,7 +3886,7 @@ 1 True - + Maximum distance at which the range finder could detect fog (m) Limit for fog detection. If the range finder measures a distance greater than this value, the measurement is considered to not be blocked by fog or rain. If there's a jump from larger than RNG_FOG to smaller than EKF2_RNG_FOG, the measurement may gets rejected. 0 - disabled 0.0 @@ -12194,7 +3940,7 @@ 3 - Minimum range validity period + Minumum range validity period Minimum duration during which the reported range finder signal quality needs to be non-zero in order to be declared valid (s) 0.1 5 @@ -12283,32 +4029,23 @@ 3 - - - Required esc bootloader version - 0 - 65535 - - - Required esc firmware version - 0 - 65535 - - - Required esc hardware version - 0 - 65535 - - RC Loss Alarm - Enable/disable event task for RC Loss. When enabled, an alarm tune will be played via buzzer or ESCs, if supported. The alarm will sound after a disarm, if the vehicle was previously armed and only if the vehicle had RC signal at some point. Particularly useful for locating crashed drones without a GPS sensor. + Enable/disable event task for RC Loss. When enabled, an alarm tune will be +played via buzzer or ESCs, if supported. The alarm will sound after a disarm, +if the vehicle was previously armed and only if the vehicle had RC signal at +some point. Particularly useful for locating crashed drones without a GPS +sensor. true Status Display - Enable/disable event task for displaying the vehicle status using arm-mounted LEDs. When enabled and if the vehicle supports it, LEDs will flash indicating various vehicle status changes. Currently PX4 has not implemented any specific status events. - + Enable/disable event task for displaying the vehicle status using arm-mounted +LEDs. When enabled and if the vehicle supports it, LEDs will flash +indicating various vehicle status changes. Currently PX4 has not implemented +any specific status events. +- true @@ -12333,7 +4070,8 @@ Maximum manually added yaw rate - This is the maximally added yaw rate setpoint from the yaw stick in any attitude controlled flight mode. It is added to the yaw rate setpoint generated by the controller for turn coordination. + This is the maximally added yaw rate setpoint from the yaw stick in any attitude controlled flight mode. +It is added to the yaw rate setpoint generated by the controller for turn coordination. 0 deg/s 1 @@ -12341,7 +4079,9 @@ Pitch setpoint offset (pitch at level flight) - An airframe specific offset of the pitch setpoint in degrees, the value is added to the pitch setpoint and should correspond to the pitch at typical cruise speed of the airframe. + An airframe specific offset of the pitch setpoint in degrees, the value is +added to the pitch setpoint and should correspond to the pitch at +typical cruise speed of the airframe. -90.0 90.0 deg @@ -12366,7 +4106,8 @@ Attitude pitch time constant - This defines the latency between a pitch step input and the achieved setpoint (inverse to a P gain). Smaller systems may require smaller values. + This defines the latency between a pitch step input and the achieved setpoint +(inverse to a P gain). Smaller systems may require smaller values. 0.2 1.0 s @@ -12383,21 +4124,14 @@ Attitude Roll Time Constant - This defines the latency between a roll step input and the achieved setpoint (inverse to a P gain). Smaller systems may require smaller values. + This defines the latency between a roll step input and the achieved setpoint +(inverse to a P gain). Smaller systems may require smaller values. 0.2 1.0 s 2 0.05 - - Spoiler landing setting - 0.0 - 1.0 - norm - 2 - 0.01 - Wheel steering rate feed forward 0.0 @@ -12408,7 +4142,8 @@ Wheel steering rate integrator gain - This gain defines how much control response will result out of a steady state error. It trims any constant error. + This gain defines how much control response will result out of a steady +state error. It trims any constant error. 0.0 10 %/rad @@ -12424,7 +4159,8 @@ Wheel steering rate proportional gain - This defines how much the wheel steering input will be commanded depending on the current body angular rate error. + This defines how much the wheel steering input will be commanded depending on the +current body angular rate error. 0.0 10 %/rad/s @@ -12433,11 +4169,13 @@ Enable wheel steering controller - Only enabled during automatic runway takeoff and landing. In all manual modes the wheel is directly controlled with yaw stick. + Only enabled during automatic runway takeoff and landing. +In all manual modes the wheel is directly controlled with yaw stick. Maximum wheel steering rate - This limits the maximum wheel steering rate the controller will output (in degrees per second). + This limits the maximum wheel steering rate the controller will output (in degrees per +second). 0.0 90.0 deg/s @@ -12454,9 +4192,25 @@ + + Flaps setting during landing + Sets a fraction of full flaps during landing. +Also applies to flaperons if enabled in the mixer/allocation. + 0.0 + 1.0 + norm + 2 + 0.01 + Bit mask to set the automatic landing abort conditions - Terrain estimation: bit 0: Abort if terrain is not found bit 1: Abort if terrain times out (after a first successful measurement) The last estimate is always used as ground, whether the last valid measurement or the land waypoint, depending on the selected abort criteria, until an abort condition is entered. If FW_LND_USETER == 0, these bits are ignored. TODO: Extend automatic abort conditions e.g. glide slope tracking error (horizontal and vertical) + Terrain estimation: +bit 0: Abort if terrain is not found +bit 1: Abort if terrain times out (after a first successful measurement) +The last estimate is always used as ground, whether the last valid measurement or the land waypoint, depending on the +selected abort criteria, until an abort condition is entered. If FW_LND_USETER == 0, these bits are ignored. +TODO: Extend automatic abort conditions +e.g. glide slope tracking error (horizontal and vertical) 0 3 @@ -12466,7 +4220,8 @@ Landing airspeed - The calibrated airspeed setpoint during landing. If set <= 0, landing airspeed = FW_AIRSPD_MIN by default. + The calibrated airspeed setpoint during landing. +If set <= 0, landing airspeed = FW_AIRSPD_MIN by default. -1.0 m/s 1 @@ -12474,16 +4229,19 @@ Maximum landing slope angle - Typically the desired landing slope angle when landing configuration (flaps, airspeed) is enabled. Set this value within the vehicle's performance limits. + Typically the desired landing slope angle when landing configuration (flaps, airspeed) is enabled. +Set this value within the vehicle's performance limits. 1.0 - 15.0 + 45.0 deg 1 0.5 Early landing configuration deployment - Allows to deploy the landing configuration (flaps, landing airspeed, etc.) already in the loiter-down waypoint before the final approach. Otherwise is enabled only in the final approach. + Allows to deploy the landing configuration (flaps, landing airspeed, etc.) already in +the loiter-down waypoint before the final approach. +Otherwise is enabled only in the final approach. Landing flare altitude (relative to landing altitude) @@ -12522,7 +4280,9 @@ Landing flare time - Multiplied by the descent rate to calculate a dynamic altitude at which to trigger the flare. NOTE: max(FW_LND_FLALT, FW_LND_FL_TIME * descent rate) is taken as the flare altitude + Multiplied by the descent rate to calculate a dynamic altitude at which +to trigger the flare. +NOTE: max(FW_LND_FLALT, FW_LND_FL_TIME * descent rate) is taken as the flare altitude 0.1 5.0 s @@ -12531,7 +4291,11 @@ Landing touchdown nudging option - Approach angle nudging: shifts the touchdown point laterally while keeping the approach entrance point constant Approach path nudging: shifts the touchdown point laterally along with the entire approach path This is useful for manually adjusting the landing point in real time when map or GNSS errors cause an offset from the desired landing vector. Nudging is done with yaw stick, constrained to FW_LND_TD_OFF (in meters) and the direction is relative to the vehicle heading (stick deflection to the right = land point moves to the right as seen by the vehicle). + Approach angle nudging: shifts the touchdown point laterally while keeping the approach entrance point constant +Approach path nudging: shifts the touchdown point laterally along with the entire approach path +This is useful for manually adjusting the landing point in real time when map or GNSS errors cause an offset from the +desired landing vector. Nudging is done with yaw stick, constrained to FW_LND_TD_OFF (in meters) and the direction is +relative to the vehicle heading (stick deflection to the right = land point moves to the right as seen by the vehicle). 0 2 @@ -12540,48 +4304,247 @@ Nudge approach path - - Maximum lateral position offset for the touchdown point - 0.0 - 10.0 - m - 1 - 1 + + Maximum lateral position offset for the touchdown point + 0.0 + 10.0 + m + 1 + 1 + + + Landing touchdown time (since flare start) + This is the time after the start of flaring that we expect the vehicle to touch the runway. +At this time, a 0.5s clamp down ramp will engage, constraining the pitch setpoint to RWTO_PSP. +If enabled, ensure that RWTO_PSP is configured appropriately for full gear contact on ground roll. +Set to -1.0 to disable touchdown clamping. E.g. it may not be desirable to clamp on belly landings. +The touchdown time will be constrained to be greater than or equal to the flare time (FW_LND_FL_TIME). + -1.0 + 5.0 + s + 1 + 0.1 + + + Altitude time constant factor for landing and low-height flight + The TECS altitude time constant (FW_T_ALT_TC) is multiplied by this value. + 0.2 + 1.0 + + 1 + 0.1 + + + Use terrain estimation during landing + This is critical for detecting when to flare, and should be enabled if possible. +If enabled and no measurement is found within a given timeout, the landing waypoint altitude will be used OR the landing +will be aborted, depending on the criteria set in FW_LND_ABORT. +If disabled, FW_LND_ABORT terrain based criteria are ignored. + 0 + 2 + + Disable the terrain estimate + Use the terrain estimate to trigger the flare (only) + Calculate landing glide slope relative to the terrain estimate + + + + Spoiler landing setting + 0.0 + 1.0 + norm + 2 + 0.01 + + + + + Flaps setting during take-off + Sets a fraction of full flaps during take-off. +Also applies to flaperons if enabled in the mixer/allocation. + 0.0 + 1.0 + norm + 2 + 0.01 + + + Trigger time + Launch is detected when acceleration in body forward direction is above FW_LAUN_AC_THLD for FW_LAUN_AC_T seconds. + 0.0 + 5.0 + s + 2 + 0.05 + + + Trigger acceleration threshold + Launch is detected when acceleration in body forward direction is above FW_LAUN_AC_THLD for FW_LAUN_AC_T seconds. + 0 + m/s^2 + 1 + 0.5 + + + Control surface launch delay + Locks control surfaces during pre-launch (armed) and until this time since launch has passed. +Only affects control surfaces that have corresponding flag set, and not active for runway takeoff. +Set to 0 to disable any surface locking after arming. + 0.0 + s + 1 + 0.1 + + + Fixed-wing launch detection + Enables automatic launch detection based on measured acceleration. Use for hand- or catapult-launched vehicles. +Not compatible with runway takeoff. + + + Motor delay + Start the motor(s) this amount of seconds after launch is detected. + 0.0 + 10.0 + s + 1 + 0.5 + + + Takeoff Airspeed + The calibrated airspeed setpoint during the takeoff climbout. +If set <= 0, FW_AIRSPD_MIN will be set by default. + -1.0 + m/s + 1 + 0.1 + + + Minimum pitch during takeoff + -5.0 + 80.0 + deg + 1 + 0.5 + + + + + GPS failure loiter time + The time the system should do open loop loiter and wait for GPS recovery +before it starts descending. Set to 0 to disable. Roll angle is set to FW_GPSF_R. +Does only apply for fixed-wing vehicles or VTOLs with NAV_FORCE_VT set to 0. + 0 + s + + + GPS failure fixed roll angle + Roll angle in GPS failure loiter mode. + 0.0 + 60.0 + deg + 1 + 0.5 + + + Custom stick configuration + Applies in manual Position and Altitude flight modes. + 0 + 3 + + Alternative stick configuration (height rate on throttle stick, airspeed on pitch stick) + Enable airspeed setpoint via sticks in altitude and position flight mode + + + + Maximum pitch angle setpoint + Applies in any altitude controlled flight mode. + 0.0 + 80.0 + deg + 1 + 0.5 + + + Minimum pitch angle setpoint + Applies in any altitude controlled flight mode. + -60.0 + 0.0 + deg + 1 + 0.5 + + + Maximum roll angle setpoint + Applies in any altitude controlled flight mode. + 35.0 + 75.0 + deg + 1 + 0.5 + + + Idle throttle + This is the minimum throttle while on the ground ("landed") in auto modes. + 0.0 + 1.0 + norm + 2 + 0.01 + + + Throttle limit max + Applies in any altitude controlled flight mode. +Should be set accordingly to achieve FW_T_CLMB_MAX. + 0.0 + 1.0 + norm + 2 + 0.01 + + + Throttle limit min + Applies in any altitude controlled flight mode. +Usually set to 0 but can be increased to prevent the motor from stopping when +descending, which can increase achievable descent rates. + 0.0 + 1.0 + norm + 2 + 0.01 + + + Default target climbrate + In auto modes: default climb rate output by controller to achieve altitude setpoints. +In manual modes: maximum climb rate setpoint. + 0.1 + m/s + 2 + 0.01 - - Landing touchdown time (since flare start) - This is the time after the start of flaring that we expect the vehicle to touch the runway. At this time, a 0.5s clamp down ramp will engage, constraining the pitch setpoint to RWTO_PSP. If enabled, ensure that RWTO_PSP is configured appropriately for full gear contact on ground roll. Set to -1.0 to disable touchdown clamping. E.g. it may not be desirable to clamp on belly landings. The touchdown time will be constrained to be greater than or equal to the flare time (FW_LND_FL_TIME). - -1.0 - 5.0 - s - 1 - 0.1 + + Default target sinkrate + In auto modes: default sink rate output by controller to achieve altitude setpoints. +In manual modes: maximum sink rate setpoint. + 0.1 + m/s + 2 + 0.01 - - Altitude time constant factor for landing and low-height flight - The TECS altitude time constant (FW_T_ALT_TC) is multiplied by this value. - 0.2 - 1.0 - + + Speed <--> Altitude weight + Adjusts the amount of weighting that the pitch control +applies to speed vs height errors. +0 -> control height only +2 -> control speed only (gliders) + 0.0 + 2.0 1 - 0.1 - - - Use terrain estimation during landing - This is critical for detecting when to flare, and should be enabled if possible. If enabled and no measurement is found within a given timeout, the landing waypoint altitude will be used OR the landing will be aborted, depending on the criteria set in FW_LND_ABORT. If disabled, FW_LND_ABORT terrain based criteria are ignored. - 0 - 2 - - Disable the terrain estimate - Use the terrain estimate to trigger the flare (only) - Calculate landing glide slope relative to the terrain estimate - + 1.0 - - Height (AGL) of the wings when the aircraft is on the ground - This is used to constrain a minimum altitude below which we keep wings level to avoid wing tip strike. It's safer to give a slight margin here (> 0m) + This is used to constrain a minimum altitude below which we keep wings level to avoid wing tip strike. It's safer +to give a slight margin here (> 0m) 0.0 m 1 @@ -12596,37 +4559,188 @@ 0.1 - - - Trigger time - Launch is detected when acceleration in body forward direction is above FW_LAUN_AC_THLD for FW_LAUN_AC_T seconds. + + + Path navigation roll slew rate limit + Maximum change in roll angle setpoint per second. +Applied in all Auto modes, plus manual Position & Altitude modes. + 0 + deg/s + 0 + 1 + + + + + Minimum groundspeed + The controller will increase the commanded airspeed to maintain +this minimum groundspeed to the next waypoint. 0.0 - 5.0 - s + 40 + m/s + 1 + 0.5 + + + Throttle max slew rate + Maximum slew rate for the commanded throttle + 0.0 + 1.0 + 2 + 0.01 + + + Altitude error time constant + 2.0 + 2 + 0.5 + + + Fast descend: minimum altitude error + Minimum altitude error needed to descend with max airspeed and minimal throttle. +A negative value disables fast descend. + -1.0 + 0 + + + Height rate feed forward + 0.0 + 1.0 2 0.05 - - Trigger acceleration threshold - Launch is detected when acceleration in body forward direction is above FW_LAUN_AC_THLD for FW_LAUN_AC_T seconds. - 0 - m/s^2 + + Integrator gain pitch + Increase it to trim out speed and height offsets faster, +with the downside of possible overshoots and oscillations. + 0.0 + 2.0 + 2 + 0.05 + + + Pitch damping gain + 0.0 + 2.0 + 2 + 0.1 + + + Roll -> Throttle feedforward + Is used to compensate for the additional drag created by turning. +Increase this gain if the aircraft initially loses energy in turns +and reduce if the aircraft initially gains energy in turns. + 0.0 + 20.0 1 0.5 - - Fixed-wing launch detection - Enables automatic launch detection based on measured acceleration. Use for hand- or catapult-launched vehicles. Not compatible with runway takeoff. + + Specific total energy balance rate feedforward gain + 0.5 + 3 + 2 + 0.01 - - Motor delay - Start the motor(s) this amount of seconds after launch is detected. + + Maximum descent rate + 1.0 + 15.0 + m/s + 1 + 0.5 + + + Airspeed rate measurement standard deviation + For the airspeed filter in TECS. + 0.01 + 10.0 + m/s^2 + 2 + 0.1 + + + Process noise standard deviation for the airspeed rate + This is defining the noise in the airspeed rate for the constant airspeed rate model +of the TECS airspeed filter. + 0.01 + 10.0 + m/s^2 + 2 + 0.1 + + + Airspeed measurement standard deviation + For the airspeed filter in TECS. + 0.01 + 10.0 + m/s + 2 + 0.1 + + + Specific total energy rate first order filter time constant + This filter is applied to the specific total energy rate used for throttle damping. + 0.0 + 2 + 2 + 0.01 + + + True airspeed error time constant + 2.0 + 2 + 0.5 + + + Throttle damping factor + This is the damping gain for the throttle demand loop. + 0.0 + 1.0 + 3 + 0.01 + + + Integrator gain throttle + Increase it to trim out speed and height offsets faster, +with the downside of possible overshoots and oscillations. 0.0 + 1.0 + 3 + 0.005 + + + Low-height threshold for tighter altitude tracking + Height above ground threshold below which tighter altitude +tracking gets enabled (see FW_LND_THRTC_SC). Below this height, TECS smoothly +(1 sec / sec) transitions the altitude tracking time constant from FW_T_ALT_TC +to FW_LND_THRTC_SC*FW_T_ALT_TC. +-1 to disable. + -1 + m + 0 + 1 + + + Maximum vertical acceleration + This is the maximum vertical acceleration +either up or down that the controller will use to correct speed +or height errors. + 1.0 10.0 - s + m/s^2 1 0.5 + + Wind-based airspeed scaling factor + Multiplying this factor with the current absolute wind estimate gives the airspeed offset +added to the minimum airspeed setpoint limit. This helps to make the +system more robust against disturbances (turbulence) in high wind. + 0 + 2 + 0.01 + @@ -12637,21 +4751,10 @@ 2 0.01 - - Enable minimum forward ground speed maintaining excess wind handling logic - - - Maximum, minimum forward ground speed for track keeping in excess wind - The maximum value of the minimum forward ground speed that may be commanded by the track keeping excess wind handling logic. Commanded in full at the normalized track error fraction of the track error boundary and reduced to zero on track. - 0.0 - 10.0 - m/s - 1 - 0.5 - Enable automatic lower bound on the NPFG period - Avoids limit cycling from a too aggressively tuned period/damping combination. If false, also disables upper bound NPFG_PERIOD_UB. + Avoids limit cycling from a too aggressively tuned period/damping combination. +If false, also disables upper bound NPFG_PERIOD_UB. NPFG period @@ -12664,7 +4767,8 @@ Period safety factor - Multiplied by period for conservative minimum period bounding (when period lower bounding is enabled). 1.0 bounds at marginal stability. + Multiplied by period for conservative minimum period bounding (when period lower +bounding is enabled). 1.0 bounds at marginal stability. 1.0 10.0 1 @@ -12672,7 +4776,8 @@ Roll time constant - Time constant of roll controller command / response, modeled as first order delay. Used to determine lower period bound. Setting zero disables automatic period bounding. + Time constant of roll controller command / response, modeled as first order delay. +Used to determine lower period bound. Setting zero disables automatic period bounding. 0.00 2.00 s @@ -12681,62 +4786,27 @@ NPFG switch distance multiplier - Multiplied by the track error boundary to determine when the aircraft switches to the next waypoint and/or path segment. Should be less than 1. + Multiplied by the track error boundary to determine when the aircraft switches +to the next waypoint and/or path segment. Should be less than 1. 0.1 1.0 - 2 - 0.01 - - - Enable track keeping excess wind handling logic - - - Enable automatic upper bound on the NPFG period - Adapts period to maintain track keeping in variable winds and path curvature. - - - Enable wind excess regulation - Disabling this parameter further disables all other airspeed incrementation options. - - - - - Path navigation roll slew rate limit - Maximum change in roll angle setpoint per second. Applied in all Auto modes, plus manual Position & Altitude modes. - 0 - deg/s - 0 - 1 - - - Custom stick configuration - Applies in manual Position and Altitude flight modes. - 0 - 3 - - Alternative stick configuration (height rate on throttle stick, airspeed on pitch stick) - Enable airspeed setpoint via sticks in altitude and position flight mode - - - - Maximum roll angle setpoint - Applies in any altitude controlled flight mode. - 35.0 - 65.0 - deg - 1 - 0.5 - - - Minimum pitch during takeoff - -5.0 - 30.0 - deg - 1 - 0.5 + 2 + 0.01 + + + Enable automatic upper bound on the NPFG period + Adapts period to maintain track keeping in variable winds and path curvature. + + Airspeed scale with full flaps + Factor applied to the minimum and stall airspeed when flaps are fully deployed. + 0.5 + 1 + 2 + 0.01 + Maximum Airspeed (CAS) The maximal airspeed (calibrated airspeed) the user is able to command. @@ -12747,7 +4817,13 @@ Minimum Airspeed (CAS) - The minimal airspeed (calibrated airspeed) the user is able to command. Further, if the airspeed falls below this value, the TECS controller will try to increase airspeed more aggressively. Has to be set according to the vehicle's stall speed (which should be set in FW_AIRSPD_STALL), with some margin between the stall speed and minimum airspeed. This value corresponds to the desired minimum speed with the default load factor (level flight, default weight), and is automatically adpated to the current load factor (calculated from roll setpoint and WEIGHT_GROSS/WEIGHT_BASE). + The minimal airspeed (calibrated airspeed) the user is able to command. +Further, if the airspeed falls below this value, the TECS controller will try to +increase airspeed more aggressively. +Has to be set according to the vehicle's stall speed (which should be set in FW_AIRSPD_STALL), +with some margin between the stall speed and minimum airspeed. +This value corresponds to the desired minimum speed with the default load factor (level flight, default weight), +and is automatically adpated to the current load factor (calculated from roll setpoint and WEIGHT_GROSS/WEIGHT_BASE). 0.5 m/s 1 @@ -12755,7 +4831,9 @@ Stall Airspeed (CAS) - The stall airspeed (calibrated airspeed) of the vehicle. It is used for airspeed sensor failure detection and for the control surface scaling airspeed limits. + The stall airspeed (calibrated airspeed) of the vehicle. +It is used for airspeed sensor failure detection and for the control +surface scaling airspeed limits. 0.5 m/s 1 @@ -12763,7 +4841,9 @@ Trim (Cruise) Airspeed - The trim CAS (calibrated airspeed) of the vehicle. If an airspeed controller is active, this is the default airspeed setpoint that the controller will try to achieve. This value corresponds to the trim airspeed with the default load factor (level flight, default weight). + The trim CAS (calibrated airspeed) of the vehicle. If an airspeed controller is active, +this is the default airspeed setpoint that the controller will try to achieve. +This value corresponds to the trim airspeed with the default load factor (level flight, default weight). 0.5 m/s 1 @@ -12771,7 +4851,9 @@ Service ceiling - Altitude in standard atmosphere at which the vehicle in normal configuration (WEIGHT_BASE) is still able to achieve a maximum climb rate of 0.5m/s at maximum throttle (FW_THR_MAX). Used to compensate for air density in FW_T_CLMB_MAX. Set negative to disable. + Altitude in standard atmosphere at which the vehicle in normal configuration (WEIGHT_BASE) is still able to achieve a maximum climb rate of +0.5m/s at maximum throttle (FW_THR_MAX). Used to compensate for air density in FW_T_CLMB_MAX. +Set negative to disable. -1.0 m 0 @@ -12779,7 +4861,8 @@ Throttle at max airspeed - Required throttle (at sea level, standard atmosphere) for level flight at maximum airspeed FW_AIRSPD_MAX Set to 0 to disable mapping of airspeed to trim throttle. + Required throttle (at sea level, standard atmosphere) for level flight at maximum airspeed FW_AIRSPD_MAX +Set to 0 to disable mapping of airspeed to trim throttle. 0 1 2 @@ -12787,7 +4870,8 @@ Throttle at min airspeed - Required throttle (at sea level, standard atmosphere) for level flight at minimum airspeed FW_AIRSPD_MIN Set to 0 to disable mapping of airspeed to trim throttle below FW_AIRSPD_TRIM. + Required throttle (at sea level, standard atmosphere) for level flight at minimum airspeed FW_AIRSPD_MIN +Set to 0 to disable mapping of airspeed to trim throttle below FW_AIRSPD_TRIM. 0 1 2 @@ -12804,32 +4888,38 @@ Maximum climb rate - This is the maximum calibrated climb rate that the aircraft can achieve with the throttle set to FW_THR_MAX and the airspeed set to the trim value. For electric aircraft make sure this number can be achieved towards the end of flight when the battery voltage has reduced. + This is the maximum calibrated climb rate that the aircraft can achieve with +the throttle set to FW_THR_MAX and the airspeed set to the +trim value. For electric aircraft make sure this number can be +achieved towards the end of flight when the battery voltage has reduced. 1.0 - 15.0 m/s 1 0.5 Minimum descent rate - This is the minimum calibrated sink rate of the aircraft with the throttle set to THR_MIN and flown at the same airspeed as used to measure FW_T_CLMB_MAX. + This is the minimum calibrated sink rate of the aircraft with the throttle +set to THR_MIN and flown at the same airspeed as used +to measure FW_T_CLMB_MAX. 1.0 - 5.0 m/s 1 0.5 Vehicle base weight - This is the weight of the vehicle at which it's performance limits were derived. A zero or negative value disables trim throttle and minimum airspeed compensation based on weight. + This is the weight of the vehicle at which it's performance limits were derived. A zero or negative value +disables trim throttle and minimum airspeed compensation based on weight. kg 1 0.5 Vehicle gross weight - This is the actual weight of the vehicle at any time. This value will differ from WEIGHT_BASE in case weight was added or removed from the base weight. Examples are the addition of payloads or larger batteries. A zero or negative value disables trim throttle and minimum airspeed compensation based on weight. + This is the actual weight of the vehicle at any time. This value will differ from WEIGHT_BASE in case weight was added +or removed from the base weight. Examples are the addition of payloads or larger batteries. A zero or negative value +disables trim throttle and minimum airspeed compensation based on weight. kg 1 0.1 @@ -12844,7 +4934,10 @@ Enable yaw rate controller in Acro - If this parameter is set to 1, the yaw rate controller is enabled in Fixed-wing Acro mode. Otherwise the pilot commands directly the yaw actuator. It is disabled by default because an active yaw rate controller will fight against the natural turn coordination of the plane. + If this parameter is set to 1, the yaw rate controller is enabled in Fixed-wing Acro mode. +Otherwise the pilot commands directly the yaw actuator. +It is disabled by default because an active yaw rate controller will fight against the +natural turn coordination of the plane. Acro body pitch max rate setpoint @@ -12860,11 +4953,16 @@ Enable airspeed scaling - This enables a logic that automatically adjusts the output of the rate controller to take into account the real torque produced by an aerodynamic control surface given the current deviation from the trim airspeed (FW_AIRSPD_TRIM). Enable when using aerodynamic control surfaces (e.g.: plane) Disable when using rotor wings (e.g.: autogyro) + This enables a logic that automatically adjusts the output of the rate controller to take +into account the real torque produced by an aerodynamic control surface given +the current deviation from the trim airspeed (FW_AIRSPD_TRIM). +Enable when using aerodynamic control surfaces (e.g.: plane) +Disable when using rotor wings (e.g.: autogyro) Enable throttle scale by battery level - This compensates for voltage drop of the battery over time by attempting to normalize performance across the operating range of the battery. + This compensates for voltage drop of the battery over time by attempting to +normalize performance across the operating range of the battery. Pitch trim increment at maximum airspeed @@ -12914,27 +5012,21 @@ 2 0.01 - - Flaps setting during landing - Sets a fraction of full flaps during landing. Also applies to flaperons if enabled in the mixer/allocation. - 0.0 - 1.0 - norm - 2 - 0.01 + + Enable rate gain compression - - Flaps setting during take-off - Sets a fraction of full flaps during take-off. Also applies to flaperons if enabled in the mixer/allocation. + + Compression gain lower limit + The range of the compression gain is between this parameter and 1.0 0.0 1.0 - norm 2 0.01 Manual pitch scale - Scale factor applied to the desired pitch actuator command in full manual mode. This parameter allows to adjust the throws of the control surfaces. + Scale factor applied to the desired pitch actuator command in full manual mode. This parameter allows +to adjust the throws of the control surfaces. 0.0 norm 2 @@ -12942,7 +5034,8 @@ Manual roll scale - Scale factor applied to the desired roll actuator command in full manual mode. This parameter allows to adjust the throws of the control surfaces. + Scale factor applied to the desired roll actuator command in full manual mode. This parameter allows +to adjust the throws of the control surfaces. 0.0 1.0 norm @@ -12951,7 +5044,8 @@ Manual yaw scale - Scale factor applied to the desired yaw actuator command in full manual mode. This parameter allows to adjust the throws of the control surfaces. + Scale factor applied to the desired yaw actuator command in full manual mode. This parameter allows +to adjust the throws of the control surfaces. 0.0 norm 2 @@ -13000,7 +5094,9 @@ Roll control to yaw control feedforward gain - This gain can be used to counteract the "adverse yaw" effect for fixed wings. When the plane enters a roll it will tend to yaw the nose out of the turn. This gain enables the use of a yaw actuator to counteract this effect. + This gain can be used to counteract the "adverse yaw" effect for fixed wings. +When the plane enters a roll it will tend to yaw the nose out of the turn. +This gain enables the use of a yaw actuator to counteract this effect. 0.0 1 0.01 @@ -13056,7 +5152,11 @@ Use airspeed for control - If set to 1, the airspeed measurement data, if valid, is used in the following controllers: - Rate controller: output scaling - Attitude controller: coordinated turn controller - Position controller: airspeed setpoint tracking, takeoff logic - VTOL: transition logic + If set to 1, the airspeed measurement data, if valid, is used in the following controllers: +- Rate controller: output scaling +- Attitude controller: coordinated turn controller +- Position controller: airspeed setpoint tracking, takeoff logic +- VTOL: transition logic Yaw rate derivative gain @@ -13099,251 +5199,37 @@ 0.005 - - - Minimum groundspeed - The controller will increase the commanded airspeed to maintain this minimum groundspeed to the next waypoint. - 0.0 - 40 - m/s - 1 - 0.5 - - - Maximum pitch angle setpoint - Applies in any altitude controlled flight mode. - 0.0 - 60.0 - deg - 1 - 0.5 - - - Minimum pitch angle setpoint - Applies in any altitude controlled flight mode. - -60.0 - 0.0 - deg - 1 - 0.5 - - - Idle throttle - This is the minimum throttle while on the ground ("landed") in auto modes. - 0.0 - 0.4 - norm - 2 - 0.01 - - - Throttle limit max - Applies in any altitude controlled flight mode. Should be set accordingly to achieve FW_T_CLMB_MAX. - 0.0 - 1.0 - norm - 2 - 0.01 - - - Throttle limit min - Applies in any altitude controlled flight mode. Usually set to 0 but can be increased to prevent the motor from stopping when descending, which can increase achievable descent rates. - 0.0 - 1.0 - norm - 2 - 0.01 - - - Throttle max slew rate - Maximum slew rate for the commanded throttle - 0.0 - 1.0 - 2 - 0.01 - - - Takeoff Airspeed - The calibrated airspeed setpoint during the takeoff climbout. If set <= 0, FW_AIRSPD_MIN will be set by default. - -1.0 - m/s - 1 - 0.1 - - - Altitude error time constant - 2.0 - 2 - 0.5 - - - Default target climbrate - In auto modes: default climb rate output by controller to achieve altitude setpoints. In manual modes: maximum climb rate setpoint. - 0.5 - 15 - m/s - 2 - 0.01 - - - Fast descend: minimum altitude error - Minimum altitude error needed to descend with max airspeed and minimal throttle. A negative value disables fast descend. - -1.0 - 0 - - - Height rate feed forward - 0.0 - 1.0 - 2 - 0.05 - - - Integrator gain pitch - Increase it to trim out speed and height offsets faster, with the downside of possible overshoots and oscillations. - 0.0 - 2.0 - 2 - 0.05 - - - Pitch damping gain - 0.0 - 2.0 - 2 - 0.1 - - - Roll -> Throttle feedforward - Is used to compensate for the additional drag created by turning. Increase this gain if the aircraft initially loses energy in turns and reduce if the aircraft initially gains energy in turns. - 0.0 - 20.0 - 1 - 0.5 - - - Specific total energy balance rate feedforward gain - 0.5 - 3 - 2 - 0.01 - - - Maximum descent rate - 1.0 - 15.0 - m/s - 1 - 0.5 - - - Default target sinkrate - In auto modes: default sink rate output by controller to achieve altitude setpoints. In manual modes: maximum sink rate setpoint. - 0.5 - 15 - m/s - 2 - 0.01 - - - Speed <--> Altitude weight - Adjusts the amount of weighting that the pitch control applies to speed vs height errors. 0 -> control height only 2 -> control speed only (gliders) - 0.0 - 2.0 - 1 - 1.0 - - - Airspeed rate measurement standard deviation - For the airspeed filter in TECS. - 0.01 - 10.0 - m/s^2 - 2 - 0.1 - - - Process noise standard deviation for the airspeed rate - This is defining the noise in the airspeed rate for the constant airspeed rate model of the TECS airspeed filter. - 0.01 - 10.0 - m/s^2 - 2 - 0.1 - - - Airspeed measurement standard deviation - For the airspeed filter in TECS. - 0.01 - 10.0 - m/s - 2 - 0.1 - - - Specific total energy rate first order filter time constant - This filter is applied to the specific total energy rate used for throttle damping. - 0.0 - 2 - 2 - 0.01 + + + Enable Actuator Failure check + If enabled, failure detector will verify that for motors, a minimum amount of ESC current per throttle +level is being consumed. +Otherwise this indicates an motor failure. + true - - True airspeed error time constant - 2.0 + + Overcurrent motor failure limit offset + threshold = FD_ACT_MOT_C2T * thrust + FD_ACT_HIGH_OFF + 0 + 30 + A 2 - 0.5 - - - Throttle damping factor - This is the damping gain for the throttle demand loop. - 0.0 - 1.0 - 3 - 0.01 - - - Integrator gain throttle - Increase it to trim out speed and height offsets faster, with the downside of possible overshoots and oscillations. - 0.0 - 1.0 - 3 - 0.005 - - - Low-height threshold for tighter altitude tracking - Height above ground threshold below which tighter altitude tracking gets enabled (see FW_LND_THRTC_SC). Below this height, TECS smoothly (1 sec / sec) transitions the altitude tracking time constant from FW_T_ALT_TC to FW_LND_THRTC_SC*FW_T_ALT_TC. -1 to disable. - -1 - m - 0 1 - - Maximum vertical acceleration - This is the maximum vertical acceleration either up or down that the controller will use to correct speed or height errors. - 1.0 - 10.0 - m/s^2 - 1 - 0.5 - - - Wind-based airspeed scaling factor - Multiplying this factor with the current absolute wind estimate gives the airspeed offset added to the minimum airspeed setpoint limit. This helps to make the system more robust against disturbances (turbulence) in high wind. + + Undercurrent motor failure limit offset + threshold = FD_ACT_MOT_C2T * thrust - FD_ACT_LOW_OFF 0 + 30 + A 2 - 0.01 - - - - - Enable Actuator Failure check - If enabled, failure detector will verify that for motors, a minimum amount of ESC current per throttle level is being consumed. Otherwise this indicates an motor failure. - true + 1 - - Motor Failure Current/Throttle Threshold - Motor failure triggers only below this current value + + Motor Failure Current/Throttle Scale + Determines the slope between expected steady state current and linearized, normalized thrust command. +E.g. FD_ACT_MOT_C2T A represents the expected steady state current at 100%. +FD_ACT_LOW_OFF and FD_ACT_HIGH_OFF offset the threshold from that slope. 0.0 50.0 A/% @@ -13351,17 +5237,18 @@ 1 - Motor Failure Throttle Threshold - Motor failure triggers only above this throttle value. + Motor Failure Thrust Threshold + Failure detection per motor only triggers above this thrust value. +Set to 1 to disable the detection. 0.0 1.0 norm 2 0.01 - - Motor Failure Time Threshold - Motor failure triggers only if the throttle threshold and the current to throttle threshold are violated for this time. + + Motor Failure Hysteresis Time + Motor failure only triggers after current thresholds are exceeded for this time. 10 10000 ms @@ -13369,11 +5256,13 @@ Enable checks on ESCs that report their arming state - If enabled, failure detector will verify that all the ESCs have successfully armed when the vehicle has transitioned to the armed state. Timeout for receiving an acknowledgement from the ESCs is 0.3s, if no feedback is received the failure detector will auto disarm the vehicle. + If enabled, failure detector will verify that all the ESCs have successfully armed when the vehicle has transitioned to the armed state. +Timeout for receiving an acknowledgement from the ESCs is 0.3s, if no feedback is received the failure detector will auto disarm the vehicle. Enable PWM input on for engaging failsafe from an external automatic trigger system (ATS) - Enabled on either AUX5 or MAIN5 depending on board. External ATS is required by ASTM F3322-18. + Enabled on either AUX5 or MAIN5 depending on board. +External ATS is required by ASTM F3322-18. true @@ -13384,7 +5273,12 @@ FailureDetector Max Pitch - Maximum pitch angle before FailureDetector triggers the attitude_failure flag. The flag triggers flight termination (if @CBRK_FLIGHTTERM = 0), which sets outputs to their failsafe values. On takeoff the flag triggers lockdown (irrespective of @CBRK_FLIGHTTERM), which disarms motors but does not set outputs to failsafe values. Setting this parameter to 0 disables the check + Maximum pitch angle before FailureDetector triggers the attitude_failure flag. +The flag triggers flight termination (if @CBRK_FLIGHTTERM = 0), +which sets outputs to their failsafe values. +On takeoff the flag triggers lockdown (irrespective of @CBRK_FLIGHTTERM), +which disarms motors but does not set outputs to failsafe values. +Setting this parameter to 0 disables the check 0 180 deg @@ -13399,7 +5293,12 @@ FailureDetector Max Roll - Maximum roll angle before FailureDetector triggers the attitude_failure flag. The flag triggers flight termination (if @CBRK_FLIGHTTERM = 0), which sets outputs to their failsafe values. On takeoff the flag triggers lockdown (irrespective of @CBRK_FLIGHTTERM), which disarms motors but does not set outputs to failsafe values. Setting this parameter to 0 disables the check + Maximum roll angle before FailureDetector triggers the attitude_failure flag. +The flag triggers flight termination (if @CBRK_FLIGHTTERM = 0), +which sets outputs to their failsafe values. +On takeoff the flag triggers lockdown (irrespective of @CBRK_FLIGHTTERM), +which disarms motors but does not set outputs to failsafe values. +Setting this parameter to 0 disables the check 0 180 deg @@ -13414,7 +5313,9 @@ Imbalanced propeller check threshold - Value at which the imbalanced propeller metric (based on horizontal and vertical acceleration variance) triggers a failure Setting this value to 0 disables the feature. + Value at which the imbalanced propeller metric (based on horizontal and +vertical acceleration variance) triggers a failure +Setting this value to 0 disables the feature. 0 1000 1 @@ -13436,14 +5337,17 @@ Hold Initial Heading Uncontrolled Hold Front Tangent to Circle - RC Controlled + Manually (yaw stick) Controlled Altitude control mode - Maintain altitude or track target's altitude. When maintaining the altitude, the drone can crash into terrain when the target moves uphill. When tracking the target's altitude, the follow altitude FLW_TGT_HT should be high enough to prevent terrain collisions due to GPS inaccuracies of the target. + Maintain altitude or track target's altitude. When maintaining the altitude, +the drone can crash into terrain when the target moves uphill. When tracking +the target's altitude, the follow altitude FLW_TGT_HT should be high enough +to prevent terrain collisions due to GPS inaccuracies of the target. 2D Tracking: Maintain constant altitude relative to home and track XY position only 2D + Terrain: Maintain constant altitude relative to terrain below and track XY position @@ -13458,7 +5362,11 @@ Follow Angle setting in degrees - Angle to follow the target from. 0.0 Equals straight in front of the target's course (direction of motion) and the angle increases in clockwise direction, meaning Right-side would be 90.0 degrees while Left-side is -90.0 degrees Note: When the user force sets the angle out of the min/max range, it will be wrapped (e.g. 480 -> 120) in the range to gracefully handle the out of range. + Angle to follow the target from. 0.0 Equals straight in front of the target's +course (direction of motion) and the angle increases in clockwise direction, +meaning Right-side would be 90.0 degrees while Left-side is -90.0 degrees +Note: When the user force sets the angle out of the min/max range, it will be +wrapped (e.g. 480 -> 120) in the range to gracefully handle the out of range. -180.0 180.0 @@ -13470,7 +5378,8 @@ Maximum tangential velocity setting for generating the follow orbit trajectory - This is the maximum tangential velocity the drone will circle around the target whenever an orbit angle setpoint changes. Higher value means more aggressive follow behavior. + This is the maximum tangential velocity the drone will circle around the target whenever +an orbit angle setpoint changes. Higher value means more aggressive follow behavior. 0.0 20.0 1 @@ -13482,30 +5391,21 @@ 1.0 2 - - - - Serial Configuration for Main GPS - Configure on which serial port to run Main GPS. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - + + GNSS Systems for Primary GPS (integer bitmask) - This integer bitmask controls the set of GNSS systems used by the receiver. Check your receiver's documentation on how many systems are supported to be used in parallel. Currently this functionality is just implemented for u-blox receivers. When no bits are set, the receiver's default configuration should be used. Set bits true to enable: 0 : Use GPS (with QZSS) 1 : Use SBAS (multiple GPS augmentation systems) 2 : Use Galileo 3 : Use BeiDou 4 : Use GLONASS 5 : Use NAVIC + This integer bitmask controls the set of GNSS systems used by the receiver. Check your +receiver's documentation on how many systems are supported to be used in parallel. +Currently this functionality is just implemented for u-blox receivers. +When no bits are set, the receiver's default configuration should be used. +Set bits true to enable: +0 : Use GPS (with QZSS) +1 : Use SBAS (multiple GPS augmentation systems) +2 : Use Galileo +3 : Use BeiDou +4 : Use GLONASS +5 : Use NAVIC 0 63 true @@ -13520,7 +5420,8 @@ Protocol for Main GPS - Select the GPS protocol over serial. Auto-detection will probe all protocols, and thus is a bit slower. + Select the GPS protocol over serial. +Auto-detection will probe all protocols, and thus is a bit slower. 0 7 true @@ -13534,28 +5435,19 @@ NMEA (generic) - - Serial Configuration for Secondary GPS - Configure on which serial port to run Secondary GPS. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - GNSS Systems for Secondary GPS (integer bitmask) - This integer bitmask controls the set of GNSS systems used by the receiver. Check your receiver's documentation on how many systems are supported to be used in parallel. Currently this functionality is just implemented for u-blox receivers. When no bits are set, the receiver's default configuration should be used. Set bits true to enable: 0 : Use GPS (with QZSS) 1 : Use SBAS (multiple GPS augmentation systems) 2 : Use Galileo 3 : Use BeiDou 4 : Use GLONASS 5 : Use NAVIC + This integer bitmask controls the set of GNSS systems used by the receiver. Check your +receiver's documentation on how many systems are supported to be used in parallel. +Currently this functionality is just implemented for u-blox receivers. +When no bits are set, the receiver's default configuration should be used. +Set bits true to enable: +0 : Use GPS (with QZSS) +1 : Use SBAS (multiple GPS augmentation systems) +2 : Use Galileo +3 : Use BeiDou +4 : Use GLONASS +5 : Use NAVIC 0 63 true @@ -13570,7 +5462,8 @@ Protocol for Secondary GPS - Select the GPS protocol over serial. Auto-detection will probe all protocols, and thus is a bit slower. + Select the GPS protocol over serial. +Auto-detection will probe all protocols, and thus is a bit slower. 0 6 true @@ -13584,9 +5477,21 @@ NMEA (generic) + + Wipes the flash config of UBX modules + Some UBX modules have a FLASH that allows to store persistent configuration that will be loaded on start. +PX4 does override all configuration parameters it needs in RAM, which takes precedence over the values in FLASH. +However, configuration parameters that are not overriden by PX4 can still cause unexpected problems during flight. +To avoid these kind of problems a clean config can be reached by wiping the FLASH on boot. +Note: Currently only supported on UBX. + true + Log GPS communication data - If this is set to 1, all GPS communication data will be published via uORB, and written to the log file as gps_dump message. If this is set to 2, the main GPS is configured to output RTCM data, which is then logged as gps_dump and can be used for PPK. + If this is set to 1, all GPS communication data will be published via uORB, +and written to the log file as gps_dump message. +If this is set to 2, the main GPS is configured to output RTCM data, +which is then logged as gps_dump and can be used for PPK. 0 2 @@ -13597,12 +5502,15 @@ Enable sat info (if available) - Enable publication of satellite info (ORB_ID(satellite_info)) if possible. Not available on MTK. + Enable publication of satellite info (ORB_ID(satellite_info)) if possible. +Not available on MTK. true u-blox F9P UART2 Baudrate - Select a baudrate for the F9P's UART2 port. In GPS_UBX_MODE 1, 2, and 3, the F9P's UART2 port is configured to send/receive RTCM corrections. Set this to 57600 if you want to attach a telemetry radio on UART2. + Select a baudrate for the F9P's UART2 port. +In GPS_UBX_MODE 1, 2, and 3, the F9P's UART2 port is configured to send/receive RTCM corrections. +Set this to 57600 if you want to attach a telemetry radio on UART2. 0 B/s true @@ -13621,9 +5529,18 @@ Enable I2C output protocol RTCM3X + + u-blox GPS DGNSS timeout + When set to 0 (default), default DGNSS timeout set by u-blox will be used. + 0 + 255 + s + true + u-blox GPS dynamic platform model - u-blox receivers support different dynamic platform models to adjust the navigation engine to the expected application environment. + u-blox receivers support different dynamic platform models to adjust the navigation engine to +the expected application environment. 0 9 true @@ -13635,9 +5552,42 @@ airborne with <4g acceleration + + u-blox GPS jamming detection high sensitivity mode + Enables or disables the high sensitivity mode for the u-blox jamming detection +(CFG-SEC-JAMDET_SENSITIVITY_HI). When enabled, the receiver uses a +more sensitive algorithm to detect jamming. Disabling this may reduce false +positives in electrically noisy environments. + true + + + u-blox GPS minimum satellite signal level for navigation + When set to 0 (default), default minimum satellite signal level set by u-blox wll be used. + 0 + 255 + dBHz + true + + + u-blox GPS minimum elevation for a GNSS satellite to be used in navigation + When set to 0 (default), default minimum elevation set by u-blox will be used. + 0 + 127 + deg + true + u-blox GPS Mode - Select the u-blox configuration setup. Most setups will use the default, including RTK and dual GPS without heading. If rover has RTCM corrections from a static base (or other static correction source) coming in on UART2, then select Mode 5. The Heading mode requires 2 F9P devices to be attached. The main GPS will act as rover and output heading information, whereas the secondary will act as moving base. Modes 1 and 2 require each F9P UART1 to be connected to the Autopilot. In addition, UART2 on the F9P units are connected to each other. Modes 3 and 4 only require UART1 on each F9P connected to the Autopilot or Can Node. UART RX DMA is required. RTK is still possible with this setup. + Select the u-blox configuration setup. Most setups will use the default, including RTK and +dual GPS without heading. +If rover has RTCM corrections from a static base (or other static correction source) coming in on UART2, then select Mode 5. +The Heading mode requires 2 F9P devices to be attached. The main GPS will act as rover and output +heading information, whereas the secondary will act as moving base. +Modes 1 and 2 require each F9P UART1 to be connected to the Autopilot. In addition, UART2 on the +F9P units are connected to each other. +Modes 3 and 4 only require UART1 on each F9P connected to the Autopilot or Can Node. UART RX DMA is required. +RTK is still possible with this setup. +Mode 6 is intended for use with a ground control station (not necessarily an RTK correction base). 0 1 true @@ -13648,27 +5598,49 @@ Heading (Rover With Moving Base UART1 Connected to Autopilot Or Can Node At 921600) Moving Base (Moving Base UART1 Connected to Autopilot Or Can Node At 921600) Rover with Static Base on UART2 (similar to Default, except coming in on UART2) + Ground Control Station (UART2 outputs NMEA) + + Enable MSM7 message output for PPK workflow + true + + + u-blox GPS output rate + Configure the output rate of u-blox GPS receivers (protocol v27+). +When set to 0, automatic rate selection is used based on the receiver model. +Default rates: M9N=8Hz, F9P L1L2=5Hz, F9P L1L5=5Hz, Others=10Hz. +Note: Higher rates reduce satellite count (e.g., >8Hz limits to 16 SVs on M9N). +Max rates vary by model and RTK mode: F9P L1L2=5-7Hz, F9P L1L5=7-8Hz, X20=25Hz. +High rates at 115200 baud may cause dropouts. + 0 + 25 + Hz + true + Heading/Yaw offset for dual antenna GPS - Heading offset angle for dual antenna GPS setups that support heading estimation. Set this to 0 if the antennas are parallel to the forward-facing direction of the vehicle and the rover (or Unicore primary) antenna is in front. The offset angle increases clockwise. Set this to 90 if the rover (or Unicore primary, or Septentrio Mosaic Aux) antenna is placed on the right side of the vehicle and the moving base antenna is on the left side. (Note: the Unicore primary antenna is the one connected on the right as seen from the top). + Heading offset angle for dual antenna GPS setups that support heading estimation. +Set this to 0 if the antennas are parallel to the forward-facing direction +of the vehicle and the rover (or Unicore primary) antenna is in front. +The offset angle increases clockwise. +Set this to 90 if the rover (or Unicore primary, or Septentrio Mosaic Aux) +antenna is placed on the right side of the vehicle and the moving base +antenna is on the left side. +(Note: the Unicore primary antenna is the one connected on the right as seen +from the top). 0 360 deg 3 true - - PPS Capture Enable - Enables the PPS capture module. This switches mode of FMU channel 7 to be the PPS input channel. - true - Geofence violation action - Note: Setting this value to 4 enables flight termination, which will kill the vehicle on violation of the fence. + Note: Setting this value to 4 enables flight termination, +which will kill the vehicle on violation of the fence. 0 5 @@ -13682,7 +5654,8 @@ Max horizontal distance from Home - Maximum horizontal distance in meters the vehicle can be from Home before triggering a geofence action. Disabled if 0. + Maximum horizontal distance in meters the vehicle can be from Home before triggering a geofence action. +Disabled if 0. 0 10000 m @@ -13690,7 +5663,8 @@ Max vertical distance from Home - Maximum vertical distance in meters the vehicle can be from Home before triggering a geofence action. Disabled if 0. + Maximum vertical distance in meters the vehicle can be from Home before triggering a geofence action. +Disabled if 0. 0 10000 m @@ -13698,11 +5672,16 @@ [EXPERIMENTAL] Use Pre-emptive geofence triggering - WARNING: This experimental feature may cause flyaways. Use at your own risk. Predict the motion of the vehicle and trigger the breach if it is determined that the current trajectory would result in a breach happening before the vehicle can make evasive maneuvers. The vehicle is then re-routed to a safe hold position (stop for multirotor, loiter for fixed wing). + WARNING: This experimental feature may cause flyaways. Use at your own risk. +Predict the motion of the vehicle and trigger the breach if it is determined that the current trajectory +would result in a breach happening before the vehicle can make evasive maneuvers. +The vehicle is then re-routed to a safe hold position (stop for multirotor, loiter for fixed wing). Geofence source - Select which position source should be used. Selecting GPS instead of global position makes sure that there is no dependence on the position estimator 0 = global position, 1 = GPS + Select which position source should be used. Selecting GPS instead of global position makes sure that there is +no dependence on the position estimator +0 = global position, 1 = GPS 0 1 @@ -13714,7 +5693,9 @@ Airframe selection - Defines which mixer implementation to use. Some are generic, while others are specifically fit to a certain vehicle with a fixed set of actuators. 'Custom' should only be used if noting else can be used. + Defines which mixer implementation to use. +Some are generic, while others are specifically fit to a certain vehicle with a fixed set of actuators. +'Custom' should only be used if noting else can be used. Multirotor Fixed-wing @@ -13730,11 +5711,30 @@ Helicopter (tail Servo) Helicopter (Coaxial) Rover (Mecanum) + Spacecraft 2D + Spacecraft 3D + + Control surface launch lock enabled + If actuator launch lock is enabled, this surface is kept at the disarmed value. + 0 + 255 + + Control Surface 1 + Control Surface 2 + Control Surface 3 + Control Surface 4 + Control Surface 5 + Control Surface 6 + Control Surface 7 + Control Surface 8 + + Motor failure handling mode - This is used to specify how to handle motor failures reported by failure detector. + This is used to specify how to handle motor failures +reported by failure detector. Ignore Remove first failed motor from effectiveness @@ -13742,7 +5742,8 @@ Collective pitch curve at position 0 - Defines the collective pitch at the interval position 0 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 0 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 @@ -13750,7 +5751,8 @@ Collective pitch curve at position 1 - Defines the collective pitch at the interval position 1 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 1 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 @@ -13758,7 +5760,8 @@ Collective pitch curve at position 2 - Defines the collective pitch at the interval position 2 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 2 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 @@ -13766,7 +5769,8 @@ Collective pitch curve at position 3 - Defines the collective pitch at the interval position 3 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 3 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 @@ -13774,12 +5778,38 @@ Collective pitch curve at position 4 - Defines the collective pitch at the interval position 4 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 4 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 0.1 + + Integral gain for rpm control + Same definition as the proportional gain but for integral. + 0 + 10 + 3 + 0.1 + + + Proportional gain for rpm control + Ratio between rpm error devided by 1000 to how much normalized output gets added to correct for it. +motor_command = throttle_curve + CA_HELI_RPM_P * (rpm_setpoint - rpm_measurement) / 1000 + 0 + 10 + 3 + 0.1 + + + Setpoint for main rotor rpm + Requires rpm feedback for the controller. + 100 + 10000 + 0 + 1 + Throttle curve at position 0 Defines the output throttle at the interval position 0. @@ -13822,11 +5852,19 @@ Main rotor turns counter-clockwise - Default configuration is for a clockwise turning main rotor and positive thrust of the tail rotor is expected to rotate the vehicle clockwise. Set this parameter to true if the tail rotor provides thrust in counter-clockwise direction which is mostly the case when the main rotor turns counter-clockwise. + Default configuration is for a clockwise turning main rotor and +positive thrust of the tail rotor is expected to rotate the vehicle clockwise. +Set this parameter to true if the tail rotor provides thrust in counter-clockwise direction +which is mostly the case when the main rotor turns counter-clockwise. Offset for yaw compensation based on collective pitch - This allows to specify which collective pitch command results in the least amount of rotor drag. This is used to increase the accuracy of the yaw drag torque compensation based on collective pitch by aligning the lowest rotor drag with zero compensation. For symmetric profile blades this is the command that results in exactly 0° collective blade angle. For lift profile blades this is typically a command resulting in slightly negative collective blade angle. tail_output += CA_HELI_YAW_CP_S * abs(collective_pitch - CA_HELI_YAW_CP_O) + This allows to specify which collective pitch command results in the least amount of rotor drag. +This is used to increase the accuracy of the yaw drag torque compensation based on collective pitch +by aligning the lowest rotor drag with zero compensation. +For symmetric profile blades this is the command that results in exactly 0° collective blade angle. +For lift profile blades this is typically a command resulting in slightly negative collective blade angle. +tail_output += CA_HELI_YAW_CP_S * abs(collective_pitch - CA_HELI_YAW_CP_O) -2 2 3 @@ -13834,7 +5872,9 @@ Scale for yaw compensation based on collective pitch - This allows to add a proportional factor of the collective pitch command to the yaw command. A negative value is needed when positive thrust of the tail rotor rotates the vehicle opposite to the main rotor turn direction. tail_output += CA_HELI_YAW_CP_S * abs(collective_pitch - CA_HELI_YAW_CP_O) + This allows to add a proportional factor of the collective pitch command to the yaw command. +A negative value is needed when positive thrust of the tail rotor rotates the vehicle opposite to the main rotor turn direction. +tail_output += CA_HELI_YAW_CP_S * abs(collective_pitch - CA_HELI_YAW_CP_O) -2 2 3 @@ -13842,15 +5882,39 @@ Scale for yaw compensation based on throttle - This allows to add a proportional factor of the throttle command to the yaw command. A negative value is needed when positive thrust of the tail rotor rotates the vehicle opposite to the main rotor turn direction. tail_output += CA_HELI_YAW_TH_S * throttle + This allows to add a proportional factor of the throttle command to the yaw command. +A negative value is needed when positive thrust of the tail rotor rotates the vehicle opposite to the main rotor turn direction. +tail_output += CA_HELI_YAW_TH_S * throttle -2 2 3 0.1 + + Ice shedding cycle period + Ice shedding prevents ice buildup in VTOL aircraft motors by periodically spinning inactive rotors. +When enabled (period > 0), every cycle lasts for the defined period and includes a 2-second spin at 0.01 motor output. +If period <= 0, the feature is disabled. + 0.0 + s + 1 + 0.1 + + + Throw angle of swashplate servo at maximum commands for linearization + Used to linearize mechanical output of swashplate servos to avoid axis coupling and binding with 4 servo redundancy. +This requires a symmetric setup where the servo horn is exactly centered with a 0 command. +Setting to zero disables feature. + 0 + 75 + deg + 1 + 0.1 + Control allocation method - Selects the algorithm and desaturation method. If set to Automtic, the selection is based on the airframe (CA_AIRFRAME). + Selects the algorithm and desaturation method. +If set to Automatic, the selection is based on the airframe (CA_AIRFRAME). Pseudo-inverse with output clipping Pseudo-inverse with sequential desaturation technique @@ -13859,97 +5923,133 @@ Motor 0 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 10 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 11 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 1 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 2 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 3 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 4 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 5 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 6 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 7 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 8 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 9 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 @@ -13979,7 +6079,9 @@ Thrust coefficient of rotor 0 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -13987,7 +6089,9 @@ Moment coefficient of rotor 0 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14054,7 +6158,9 @@ Thrust coefficient of rotor 10 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14062,7 +6168,9 @@ Moment coefficient of rotor 10 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14129,7 +6237,9 @@ Thrust coefficient of rotor 11 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14137,7 +6247,9 @@ Moment coefficient of rotor 11 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14204,7 +6316,9 @@ Thrust coefficient of rotor 1 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14212,7 +6326,9 @@ Moment coefficient of rotor 1 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14279,7 +6395,9 @@ Thrust coefficient of rotor 2 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14287,7 +6405,9 @@ Moment coefficient of rotor 2 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14354,7 +6474,9 @@ Thrust coefficient of rotor 3 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14362,7 +6484,9 @@ Moment coefficient of rotor 3 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14429,7 +6553,9 @@ Thrust coefficient of rotor 4 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14437,7 +6563,9 @@ Moment coefficient of rotor 4 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14504,7 +6632,9 @@ Thrust coefficient of rotor 5 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14512,7 +6642,9 @@ Moment coefficient of rotor 5 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14579,7 +6711,9 @@ Thrust coefficient of rotor 6 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14587,7 +6721,9 @@ Moment coefficient of rotor 6 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14654,7 +6790,9 @@ Thrust coefficient of rotor 7 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14662,7 +6800,9 @@ Moment coefficient of rotor 7 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14729,7 +6869,9 @@ Thrust coefficient of rotor 8 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14737,7 +6879,9 @@ Moment coefficient of rotor 8 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14804,7 +6948,9 @@ Thrust coefficient of rotor 9 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14812,7 +6958,9 @@ Moment coefficient of rotor 9 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14969,65 +7117,89 @@ Servo 0 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 1 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 2 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 3 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 4 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 5 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 6 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 7 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 @@ -15045,7 +7217,9 @@ Control Surface 0 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15100,7 +7274,9 @@ Control Surface 1 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15155,7 +7331,9 @@ Control Surface 2 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15210,7 +7388,9 @@ Control Surface 3 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15265,7 +7445,9 @@ Control Surface 4 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15320,7 +7502,9 @@ Control Surface 5 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15375,7 +7559,9 @@ Control Surface 6 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15430,7 +7616,9 @@ Control Surface 7 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15485,6 +7673,12 @@ 8 + + Control Surface slew rate for normalized flaps setpoint + 0.0 + 5.0 + 1 + Tilt 0 is used for control Define if this servo is used for additional control. @@ -15497,7 +7691,8 @@ Tilt Servo 0 Tilt Angle at Maximum - Defines the tilt angle when the servo is at the maximum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the maximum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15505,7 +7700,8 @@ Tilt Servo 0 Tilt Angle at Minimum - Defines the tilt angle when the servo is at the minimum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the minimum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15513,7 +7709,9 @@ Tilt Servo 0 Tilt Direction - Defines the direction the servo tilts towards when moving towards the maximum tilt angle. For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. + Defines the direction the servo tilts towards when moving towards the maximum tilt angle. +For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', +the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. 0 359 @@ -15533,7 +7731,8 @@ Tilt Servo 1 Tilt Angle at Maximum - Defines the tilt angle when the servo is at the maximum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the maximum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15541,7 +7740,8 @@ Tilt Servo 1 Tilt Angle at Minimum - Defines the tilt angle when the servo is at the minimum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the minimum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15549,7 +7749,9 @@ Tilt Servo 1 Tilt Direction - Defines the direction the servo tilts towards when moving towards the maximum tilt angle. For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. + Defines the direction the servo tilts towards when moving towards the maximum tilt angle. +For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', +the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. 0 359 @@ -15569,7 +7771,8 @@ Tilt Servo 2 Tilt Angle at Maximum - Defines the tilt angle when the servo is at the maximum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the maximum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15577,7 +7780,8 @@ Tilt Servo 2 Tilt Angle at Minimum - Defines the tilt angle when the servo is at the minimum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the minimum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15585,7 +7789,9 @@ Tilt Servo 2 Tilt Direction - Defines the direction the servo tilts towards when moving towards the maximum tilt angle. For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. + Defines the direction the servo tilts towards when moving towards the maximum tilt angle. +For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', +the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. 0 359 @@ -15605,7 +7811,8 @@ Tilt Servo 3 Tilt Angle at Maximum - Defines the tilt angle when the servo is at the maximum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the maximum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15613,7 +7820,8 @@ Tilt Servo 3 Tilt Angle at Minimum - Defines the tilt angle when the servo is at the minimum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the minimum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15621,7 +7829,9 @@ Tilt Servo 3 Tilt Direction - Defines the direction the servo tilts towards when moving towards the maximum tilt angle. For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. + Defines the direction the servo tilts towards when moving towards the maximum tilt angle. +For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', +the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. 0 359 @@ -15643,7 +7853,8 @@ Gate size for acceleration fusion - Sets the number of standard deviations used by the innovation consistency test. + Sets the number of standard deviations used +by the innovation consistency test. 1.0 10.0 SD @@ -15651,7 +7862,8 @@ 1-sigma initial hover thrust uncertainty - Sets the number of standard deviations used by the innovation consistency test. + Sets the number of standard deviations used +by the innovation consistency test. 0.0 1.0 normalized_thrust @@ -15659,7 +7871,9 @@ Hover thrust process noise - Reduce to make the hover thrust estimate more stable, increase if the real hover thrust is expected to change quickly over time. + Reduce to make the hover thrust estimate +more stable, increase if the real hover thrust +is expected to change quickly over time. 0.0001 1.0 normalized_thrust/s @@ -15667,7 +7881,10 @@ Max deviation from MPC_THR_HOVER - Defines the range of the hover thrust estimate around MPC_THR_HOVER. A value of 0.2 with MPC_THR_HOVER at 0.5 results in a range of [0.3, 0.7]. Set to a large value if the vehicle operates in varying physical conditions that affect the required hover thrust strongly (e.g. differently sized payloads). + Defines the range of the hover thrust estimate around MPC_THR_HOVER. +A value of 0.2 with MPC_THR_HOVER at 0.5 results in a range of [0.3, 0.7]. +Set to a large value if the vehicle operates in varying physical conditions that +affect the required hover thrust strongly (e.g. differently sized payloads). 0.01 0.4 normalized_thrust @@ -15675,7 +7892,9 @@ Horizontal velocity threshold for sensitivity reduction - Above this speed, the measurement noise is linearly increased to reduce the sensitivity of the estimator from biased measurement. Set to a low value on vehicles with large lifting surfaces. + Above this speed, the measurement noise is linearly increased +to reduce the sensitivity of the estimator from biased measurement. +Set to a low value on vehicles with large lifting surfaces. 1.0 20.0 m/s @@ -15683,62 +7902,31 @@ Vertical velocity threshold for sensitivity reduction - Above this speed, the measurement noise is linearly increased to reduce the sensitivity of the estimator from biased measurement. Set to a low value on vehicles affected by air drag when climbing or descending. + Above this speed, the measurement noise is linearly increased +to reduce the sensitivity of the estimator from biased measurement. +Set to a low value on vehicles affected by air drag when climbing or descending. 1.0 10.0 m/s 1 - - - Serial Configuration for Iridium (with MAVLink) - Configure on which serial port to run Iridium (with MAVLink). - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Satellite radio read interval. Only required to be nonzero if data is not sent using a ring call - 0 - 5000 - s - - - Iridium SBD session timeout - 0 - 300 - s - - - Time the Iridium driver will wait for additional mavlink messages to combine them into one SBD message - Value 0 turns the functionality off - 0 - 500 - ms - - Fixed-wing land detector: Max airspeed Maximum airspeed allowed in the landed state 2 - 20 + 30 m/s 1 + + Fixed-wing land detector: max rotational speed + Maximum allowed norm of the angular velocity in the landed state. +Only used if neither airspeed nor groundspeed can be used for landing detection. + deg/s + 1 + Fixed-wing land detection trigger time Time the land conditions (speeds and acceleration) have to be satisfied to detect a landing. @@ -15749,9 +7937,11 @@ Fixed-wing land detector: Max horizontal velocity threshold - Maximum horizontal velocity allowed in the landed state. A factor of 0.7 is applied in case of airspeed-less flying (either because no sensor is present or sensor data got invalid in flight). + Maximum horizontal velocity allowed in the landed state. +A factor of 0.7 is applied in case of airspeed-less flying +(either because no sensor is present or sensor data got invalid in flight). 0.5 - 10 + 20 m/s 1 @@ -15767,31 +7957,24 @@ Fixed-wing land detector: Max horizontal acceleration Maximum horizontal (x,y body axes) acceleration allowed in the landed state 2 - 15 + 30 m/s^2 1 Ground effect altitude for multicopters - The height above ground below which ground effect creates barometric altitude errors. A negative value indicates no ground effect. + The height above ground below which ground effect creates barometric altitude errors. +A negative value indicates no ground effect. -1 m 2 - Multicopter max rotation - Maximum allowed angular velocity around each axis allowed in the landed state. + Multicopter max rotational speed + Maximum allowed norm of the angular velocity (roll, pitch) in the landed state. deg/s 1 - - Multicopter land detection trigger time - Total time it takes to go through all three land detection stages: ground contact, maybe landed, landed when all necessary conditions are constantly met. - 0.1 - 10.0 - s - 1 - Multicopter max horizontal velocity Maximum horizontal velocity allowed in the landed state @@ -15800,39 +7983,48 @@ Multicopter vertical velocity threshold - Vertical velocity threshold to detect landing. Has to be set lower than the expected minimal speed for landing, which is either MPC_LAND_SPEED or MPC_LAND_CRWL. This is enforced by an automatic check. + Vertical velocity threshold to detect landing. +Has to be set lower than the expected minimal speed for landing, +which is either MPC_LAND_SPEED or MPC_LAND_CRWL. +This is enforced by an automatic check. 0 m/s 2 Total flight time in microseconds - Total flight time of this autopilot. Higher 32 bits of the value. Flight time in microseconds = (LND_FLIGHT_T_HI << 32) | LND_FLIGHT_T_LO. + Total flight time of this autopilot. Higher 32 bits of the value. +Flight time in microseconds = (LND_FLIGHT_T_HI << 32) | LND_FLIGHT_T_LO. 0 Total flight time in microseconds - Total flight time of this autopilot. Lower 32 bits of the value. Flight time in microseconds = (LND_FLIGHT_T_HI << 32) | LND_FLIGHT_T_LO. + Total flight time of this autopilot. Lower 32 bits of the value. +Flight time in microseconds = (LND_FLIGHT_T_HI << 32) | LND_FLIGHT_T_LO. 0 Acceleration uncertainty - Variance of acceleration measurement used for landing target position prediction. Higher values results in tighter following of the measurements and more lenient outlier rejection + Variance of acceleration measurement used for landing target position prediction. +Higher values results in tighter following of the measurements and more lenient outlier rejection 0.01 (m/s^2)^2 2 Landing target measurement uncertainty - Variance of the landing target measurement from the driver. Higher values result in less aggressive following of the measurement and a smoother output as well as fewer rejected measurements. + Variance of the landing target measurement from the driver. +Higher values result in less aggressive following of the measurement and a smoother output as well as fewer rejected measurements. tan(rad)^2 4 Landing target mode - Configure the mode of the landing target. Depending on the mode, the landing target observations are used differently to aid position estimation. Mode Moving: The landing target may be moving around while in the field of view of the vehicle. Landing target measurements are not used to aid positioning. Mode Stationary: The landing target is stationary. Measured velocity w.r.t. the landing target is used to aid velocity estimation. + Configure the mode of the landing target. Depending on the mode, the landing target observations are used differently to aid position estimation. +Mode Moving: The landing target may be moving around while in the field of view of the vehicle. Landing target measurements are not used to aid positioning. +Mode Stationary: The landing target is stationary. Measured velocity w.r.t. the landing target is used to aid velocity estimation. 0 1 @@ -15905,7 +8097,8 @@ Accelerometer xy noise density - Data sheet noise density = 150ug/sqrt(Hz) = 0.0015 m/s^2/sqrt(Hz) Larger than data sheet to account for tilt error. + Data sheet noise density = 150ug/sqrt(Hz) = 0.0015 m/s^2/sqrt(Hz) +Larger than data sheet to account for tilt error. 0.00001 2 m/s^2/sqrt(Hz) @@ -15991,7 +8184,16 @@ Integer bitmask controlling data fusion - Set bits in the following positions to enable: 0 : Set to true to fuse GPS data if available, also requires GPS for altitude init 1 : Set to true to fuse optical flow data if available 2 : Set to true to fuse vision position 3 : Set to true to enable landing target 4 : Set to true to fuse land detector 5 : Set to true to publish AGL as local position down component 6 : Set to true to enable flow gyro compensation 7 : Set to true to enable baro fusion default (145 - GPS, baro, land detector) + Set bits in the following positions to enable: +0 : Set to true to fuse GPS data if available, also requires GPS for altitude init +1 : Set to true to fuse optical flow data if available +2 : Set to true to fuse vision position +3 : Set to true to enable landing target +4 : Set to true to fuse land detector +5 : Set to true to publish AGL as local position down component +6 : Set to true to enable flow gyro compensation +7 : Set to true to enable baro fusion +default (145 - GPS, baro, land detector) 0 255 @@ -16099,7 +8301,8 @@ Position propagation noise density - Increase to trust measurements more. Decrease to trust model more. + Increase to trust measurements more. +Decrease to trust model more. 0 1 m/s/sqrt(Hz) @@ -16114,7 +8317,8 @@ Velocity propagation noise density - Increase to trust measurements more. Decrease to trust model more. + Increase to trust measurements more. +Decrease to trust model more. 0 1 m/s^2/sqrt(Hz) @@ -16196,36 +8400,18 @@ Broadcast heartbeats on local network for MAVLink instance 0 - This allows a ground control station to automatically find the drone on the local network. + This allows a ground control station to automatically find the drone +on the local network. Never broadcast Always broadcast Only multicast - - Serial Configuration for MAVLink (instance 0) - Configure on which serial port to run MAVLink. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - Ethernet - - Enable serial flow control for instance 0 - This is used to force flow control on or off for the the mavlink instance. By default it is auto detected. Use when auto detection fails. + This is used to force flow control on or off for the the mavlink +instance. By default it is auto detected. Use when auto detection fails. True Force off @@ -16235,12 +8421,17 @@ Enable MAVLink Message forwarding for instance 0 - If enabled, forward incoming MAVLink messages to other MAVLink ports if the message is either broadcast or the target is not the autopilot. This allows for example a GCS to talk to a camera that is connected to the autopilot via MAVLink (on a different link than the GCS). + If enabled, forward incoming MAVLink messages to other MAVLink ports if the +message is either broadcast or the target is not the autopilot. +This allows for example a GCS to talk to a camera that is connected to the +autopilot via MAVLink (on a different link than the GCS). True Configures the frequency of HIGH_LATENCY2 stream for instance 0 - Positive real value that configures the transmission frequency of the HIGH_LATENCY2 stream for instance 0, configured in iridium mode. This parameter has no effect if the instance mode is different from iridium. + Positive real value that configures the transmission frequency of the +HIGH_LATENCY2 stream for instance 0, configured in iridium mode. +This parameter has no effect if the instance mode is different from iridium. 0.0 50.0 Hz @@ -16250,7 +8441,8 @@ MAVLink Mode for instance 0 - The MAVLink Mode defines the set of streamed messages (for example the vehicle's attitude) and their sending rates. + The MAVLink Mode defines the set of streamed messages (for example the +vehicle's attitude) and their sending rates. True Normal @@ -16264,62 +8456,54 @@ Gimbal Onboard Low Bandwidth uAvionix + Low Bandwidth Enable software throttling of mavlink on instance 0 - If enabled, MAVLink messages will be throttled according to `txbuf` field reported by radio_status. Requires a radio to send the mavlink message RADIO_STATUS. + If enabled, MAVLink messages will be throttled according to +`txbuf` field reported by radio_status. +Requires a radio to send the mavlink message RADIO_STATUS. True Maximum MAVLink sending rate for instance 0 - Configure the maximum sending rate for the MAVLink streams in Bytes/sec. If the configured streams exceed the maximum rate, the sending rate of each stream is automatically decreased. If this is set to 0 a value of half of the theoretical maximum bandwidth is used. This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on 8N1-configured links). + Configure the maximum sending rate for the MAVLink streams in Bytes/sec. +If the configured streams exceed the maximum rate, the sending rate of +each stream is automatically decreased. +If this is set to 0 a value of half of the theoretical maximum bandwidth is used. +This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on +8N1-configured links). 0 B/s True MAVLink Remote Port for instance 0 - If ethernet enabled and selected as configuration for MAVLink instance 0, selected remote port will be set and used in MAVLink instance 0. + If ethernet enabled and selected as configuration for MAVLink instance 0, +selected remote port will be set and used in MAVLink instance 0. True MAVLink Network Port for instance 0 - If ethernet enabled and selected as configuration for MAVLink instance 0, selected udp port will be set and used in MAVLink instance 0. + If ethernet enabled and selected as configuration for MAVLink instance 0, +selected udp port will be set and used in MAVLink instance 0. True Broadcast heartbeats on local network for MAVLink instance 1 - This allows a ground control station to automatically find the drone on the local network. + This allows a ground control station to automatically find the drone +on the local network. Never broadcast Always broadcast Only multicast - - Serial Configuration for MAVLink (instance 1) - Configure on which serial port to run MAVLink. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - Ethernet - - Enable serial flow control for instance 1 - This is used to force flow control on or off for the the mavlink instance. By default it is auto detected. Use when auto detection fails. + This is used to force flow control on or off for the the mavlink +instance. By default it is auto detected. Use when auto detection fails. True Force off @@ -16329,12 +8513,17 @@ Enable MAVLink Message forwarding for instance 1 - If enabled, forward incoming MAVLink messages to other MAVLink ports if the message is either broadcast or the target is not the autopilot. This allows for example a GCS to talk to a camera that is connected to the autopilot via MAVLink (on a different link than the GCS). + If enabled, forward incoming MAVLink messages to other MAVLink ports if the +message is either broadcast or the target is not the autopilot. +This allows for example a GCS to talk to a camera that is connected to the +autopilot via MAVLink (on a different link than the GCS). True Configures the frequency of HIGH_LATENCY2 stream for instance 1 - Positive real value that configures the transmission frequency of the HIGH_LATENCY2 stream for instance 1, configured in iridium mode. This parameter has no effect if the instance mode is different from iridium. + Positive real value that configures the transmission frequency of the +HIGH_LATENCY2 stream for instance 1, configured in iridium mode. +This parameter has no effect if the instance mode is different from iridium. 0.0 50.0 Hz @@ -16344,7 +8533,8 @@ MAVLink Mode for instance 1 - The MAVLink Mode defines the set of streamed messages (for example the vehicle's attitude) and their sending rates. + The MAVLink Mode defines the set of streamed messages (for example the +vehicle's attitude) and their sending rates. True Normal @@ -16358,62 +8548,54 @@ Gimbal Onboard Low Bandwidth uAvionix + Low Bandwidth Enable software throttling of mavlink on instance 1 - If enabled, MAVLink messages will be throttled according to `txbuf` field reported by radio_status. Requires a radio to send the mavlink message RADIO_STATUS. + If enabled, MAVLink messages will be throttled according to +`txbuf` field reported by radio_status. +Requires a radio to send the mavlink message RADIO_STATUS. True Maximum MAVLink sending rate for instance 1 - Configure the maximum sending rate for the MAVLink streams in Bytes/sec. If the configured streams exceed the maximum rate, the sending rate of each stream is automatically decreased. If this is set to 0 a value of half of the theoretical maximum bandwidth is used. This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on 8N1-configured links). + Configure the maximum sending rate for the MAVLink streams in Bytes/sec. +If the configured streams exceed the maximum rate, the sending rate of +each stream is automatically decreased. +If this is set to 0 a value of half of the theoretical maximum bandwidth is used. +This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on +8N1-configured links). 0 B/s True MAVLink Remote Port for instance 1 - If ethernet enabled and selected as configuration for MAVLink instance 1, selected remote port will be set and used in MAVLink instance 1. + If ethernet enabled and selected as configuration for MAVLink instance 1, +selected remote port will be set and used in MAVLink instance 1. True MAVLink Network Port for instance 1 - If ethernet enabled and selected as configuration for MAVLink instance 1, selected udp port will be set and used in MAVLink instance 1. + If ethernet enabled and selected as configuration for MAVLink instance 1, +selected udp port will be set and used in MAVLink instance 1. True Broadcast heartbeats on local network for MAVLink instance 2 - This allows a ground control station to automatically find the drone on the local network. + This allows a ground control station to automatically find the drone +on the local network. Never broadcast Always broadcast Only multicast - - Serial Configuration for MAVLink (instance 2) - Configure on which serial port to run MAVLink. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - Ethernet - - Enable serial flow control for instance 2 - This is used to force flow control on or off for the the mavlink instance. By default it is auto detected. Use when auto detection fails. + This is used to force flow control on or off for the the mavlink +instance. By default it is auto detected. Use when auto detection fails. True Force off @@ -16423,12 +8605,17 @@ Enable MAVLink Message forwarding for instance 2 - If enabled, forward incoming MAVLink messages to other MAVLink ports if the message is either broadcast or the target is not the autopilot. This allows for example a GCS to talk to a camera that is connected to the autopilot via MAVLink (on a different link than the GCS). + If enabled, forward incoming MAVLink messages to other MAVLink ports if the +message is either broadcast or the target is not the autopilot. +This allows for example a GCS to talk to a camera that is connected to the +autopilot via MAVLink (on a different link than the GCS). True Configures the frequency of HIGH_LATENCY2 stream for instance 2 - Positive real value that configures the transmission frequency of the HIGH_LATENCY2 stream for instance 2, configured in iridium mode. This parameter has no effect if the instance mode is different from iridium. + Positive real value that configures the transmission frequency of the +HIGH_LATENCY2 stream for instance 2, configured in iridium mode. +This parameter has no effect if the instance mode is different from iridium. 0.0 50.0 Hz @@ -16438,7 +8625,8 @@ MAVLink Mode for instance 2 - The MAVLink Mode defines the set of streamed messages (for example the vehicle's attitude) and their sending rates. + The MAVLink Mode defines the set of streamed messages (for example the +vehicle's attitude) and their sending rates. True Normal @@ -16452,28 +8640,38 @@ Gimbal Onboard Low Bandwidth uAvionix + Low Bandwidth Enable software throttling of mavlink on instance 2 - If enabled, MAVLink messages will be throttled according to `txbuf` field reported by radio_status. Requires a radio to send the mavlink message RADIO_STATUS. + If enabled, MAVLink messages will be throttled according to +`txbuf` field reported by radio_status. +Requires a radio to send the mavlink message RADIO_STATUS. True Maximum MAVLink sending rate for instance 2 - Configure the maximum sending rate for the MAVLink streams in Bytes/sec. If the configured streams exceed the maximum rate, the sending rate of each stream is automatically decreased. If this is set to 0 a value of half of the theoretical maximum bandwidth is used. This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on 8N1-configured links). + Configure the maximum sending rate for the MAVLink streams in Bytes/sec. +If the configured streams exceed the maximum rate, the sending rate of +each stream is automatically decreased. +If this is set to 0 a value of half of the theoretical maximum bandwidth is used. +This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on +8N1-configured links). 0 B/s True MAVLink Remote Port for instance 2 - If ethernet enabled and selected as configuration for MAVLink instance 2, selected remote port will be set and used in MAVLink instance 2. + If ethernet enabled and selected as configuration for MAVLink instance 2, +selected remote port will be set and used in MAVLink instance 2. True MAVLink Network Port for instance 2 - If ethernet enabled and selected as configuration for MAVLink instance 2, selected udp port will be set and used in MAVLink instance 2. + If ethernet enabled and selected as configuration for MAVLink instance 2, +selected udp port will be set and used in MAVLink instance 2. True @@ -16484,34 +8682,49 @@ Forward external setpoint messages - If set to 1 incoming external setpoint messages will be directly forwarded to the controllers if in offboard control mode + If set to 1 incoming external setpoint messages will be directly forwarded +to the controllers if in offboard control mode Parameter hash check - Disabling the parameter hash check functionality will make the mavlink instance stream parameters continuously. + Disabling the parameter hash check functionality will make the mavlink instance +stream parameters continuously. Heartbeat message forwarding - The mavlink heartbeat message will not be forwarded if this parameter is set to 'disabled'. The main reason for disabling heartbeats to be forwarded is because they confuse dronekit. + The mavlink heartbeat message will not be forwarded if this parameter is set to 'disabled'. +The main reason for disabling heartbeats to be forwarded is because they confuse dronekit. - + MAVLink protocol version - Default to 1, switch to 2 if GCS sends version 2 - Always use version 1 - Always use version 2 + Version 1 with auto-upgrade to v2 if detected + Version 2 Timeout in seconds for the RADIO_STATUS reports coming in - If the connected radio stops reporting RADIO_STATUS for a certain time, a warning is triggered and, if MAV_X_RADIO_CTL is enabled, the software-flow control is reset. + If the connected radio stops reporting RADIO_STATUS for a certain time, +a warning is triggered and, if MAV_X_RADIO_CTL is enabled, the software-flow +control is reset. 1 250 s + + MAVLink protocol signing + + Message signing disabled + Signing enabled except on USB + Signing always enabled + + MAVLink SiK Radio ID - When non-zero the MAVLink app will attempt to configure the SiK radio to this ID and re-set the parameter to 0. If the value is negative it will reset the complete radio config to factory defaults. Only applies if this mavlink instance is going through a SiK radio + When non-zero the MAVLink app will attempt to configure the +SiK radio to this ID and re-set the parameter to 0. If the value +is negative it will reset the complete radio config to +factory defaults. Only applies if this mavlink instance is going through a SiK radio -1 240 @@ -16521,6 +8734,25 @@ 250 true + + Enable MAVLink forwarding on TELEM2 + TELEM2 on Skynode only. + True + + + MAVLink Mode for SOM to FMU communication channel + The MAVLink Mode defines the set of streamed messages (for example the +vehicle's attitude) and their sending rates. + True + + Normal + Onboard + Config + Minimal + Onboard Low Bandwidth + Low Bandwidth + + MAVLink airframe type 0 @@ -16551,50 +8783,17 @@ If set to 1 incoming HIL GPS messages are parsed. - - - BMM350 data averaging - Defines which averaging mode to use during data polling. - True - - No averaging - 2 sample averaging - 4 sample averaging - 8 sample averaging - - - - BMM350 pad drive strength setting - This setting helps avoid signal problems like overshoot or undershoot. - 0 - 7 - - - BMM350 ODR rate - Defines which ODR rate to use during data polling. - True - - 400 Hz - 200 Hz - 100 Hz - 50 Hz - 25 Hz - 12.5 Hz - 6.25 Hz - 3.125 Hz - 1.5625 Hz - - - Enable online mag bias calibration - This enables continuous calibration of the magnetometers before takeoff using gyro data. + This enables continuous calibration of the magnetometers +before takeoff using gyro data. true Mag bias estimator learning gain - Increase to make the estimator more responsive Decrease to make the estimator more robust to noise + Increase to make the estimator more responsive +Decrease to make the estimator more robust to noise 0.1 100 1 @@ -16604,11 +8803,26 @@ Enable arm/disarm stick gesture - This determines if moving the left stick to the lower right arms and to the lower left disarms the vehicle. + This determines if moving the left stick to the lower right +arms and to the lower left disarms the vehicle. + + + Deadzone for sticks (only specific use cases) + Range around stick center ignored to prevent +vehicle drift from stick hardware inaccuracy. +Does not apply to any precise constant input like +throttle and attitude or rate piloting. + 0 + 1 + 2 + 0.01 Trigger time for kill stick gesture - The timeout for holding the left stick to the lower left and the right stick to the lower right at the same time until the gesture kills the actuators one-way. A negative value disables the feature. + The timeout for holding the left stick to the lower left +and the right stick to the lower right at the same time until the gesture +kills the actuators one-way. +A negative value disables the feature. -1 15 s @@ -16616,32 +8830,24 @@ - - GPS failure loiter time - The time the system should do open loop loiter and wait for GPS recovery before it starts descending. Set to 0 to disable. Roll angle is set to FW_GPSF_R. Does only apply for fixed-wing vehicles or VTOLs with NAV_FORCE_VT set to 0. - 0 - 3600 - s - - - GPS failure fixed roll angle - Roll angle in GPS failure loiter mode. - 0.0 - 30.0 - deg - 1 - 0.5 - Timeout to allow the payload to execute the mission command - Ensure: gripper: NAV_CMD_DO_GRIPPER has released before continuing mission. winch: CMD_DO_WINCH has delivered before continuing mission. gimbal: CMD_DO_GIMBAL_MANAGER_PITCHYAW has reached the commanded orientation before beginning to take pictures. + Ensure: +gripper: NAV_CMD_DO_GRIPPER +has released before continuing mission. +winch: CMD_DO_WINCH +has delivered before continuing mission. +gimbal: CMD_DO_GIMBAL_MANAGER_PITCHYAW +has reached the commanded orientation before beginning to take pictures. 0 s 1 Maximal horizontal distance from Home to first waypoint - There will be a warning message if the current waypoint is more distant than MIS_DIST_1WP from Home. Has no effect on mission validity. Set a value of zero or less to disable. + There will be a warning message if the current waypoint is more distant than MIS_DIST_1WP from Home. +Has no effect on mission validity. +Set a value of zero or less to disable. -1 100000 m @@ -16650,13 +8856,16 @@ Landing abort min altitude - Minimum altitude above landing point that the vehicle will climb to after an aborted landing. Then vehicle will loiter in this altitude until further command is received. Only applies to fixed-wing vehicles. + Minimum altitude above landing point that the vehicle will climb to after an aborted landing. +Then vehicle will loiter in this altitude until further command is received. +Only applies to fixed-wing vehicles. 0 m Enable yaw control of the mount. (Only affects multicopters and ROI mission items) - If enabled, yaw commands will be sent to the mount and the vehicle will follow its heading towards the flight direction. If disabled, the vehicle will yaw towards the ROI. + If enabled, yaw commands will be sent to the mount and the vehicle will follow its heading towards the flight direction. +If disabled, the vehicle will yaw towards the ROI. 0 1 @@ -16666,7 +8875,8 @@ Default take-off altitude - This is the relative altitude the system will take off to if not otherwise specified. + This is the relative altitude the system will take off to +if not otherwise specified. 0 m 1 @@ -16674,7 +8884,8 @@ Mission takeoff/landing required - Specifies if a mission has to contain a takeoff and/or mission landing. Validity of configured takeoffs/landings is checked independently of the setting here. + Specifies if a mission has to contain a takeoff and/or mission landing. +Validity of configured takeoffs/landings is checked independently of the setting here. No requirements Require a takeoff @@ -16694,7 +8905,10 @@ Time in seconds we wait on reaching target heading at a waypoint if it is forced - If set > 0 it will ignore the target heading for normal waypoint acceptance. If the waypoint forces the heading the timeout will matter. For example on VTOL forwards transition. Mainly useful for VTOLs that have less yaw authority and might not reach target yaw in wind. Disabled by default. + If set > 0 it will ignore the target heading for normal waypoint acceptance. If the +waypoint forces the heading the timeout will matter. For example on VTOL forwards transition. +Mainly useful for VTOLs that have less yaw authority and might not reach target +yaw in wind. Disabled by default. -1 20 s @@ -16716,7 +8930,8 @@ Acceptance Radius - Default acceptance radius, overridden by acceptance radius of waypoint if set. For fixed wing the npfg switch distance is used for horizontal acceptance. + Default acceptance radius, overridden by acceptance radius of waypoint if set. +For fixed wing the npfg switch distance is used for horizontal acceptance. 0.05 200.0 m @@ -16728,7 +8943,8 @@ FW Altitude Acceptance Radius before a landing - Altitude acceptance used for the last waypoint before a fixed-wing landing. This is usually smaller than the standard vertical acceptance because close to the ground higher accuracy is required. + Altitude acceptance used for the last waypoint before a fixed-wing landing. This is usually smaller +than the standard vertical acceptance because close to the ground higher accuracy is required. 0.05 200.0 m @@ -16745,9 +8961,11 @@ Loiter radius (FW only) - Default value of loiter radius in FW mode (e.g. for Loiter mode). - 25 - 1000 + Default value of loiter radius in fixed-wing mode (e.g. for Loiter mode). +The direction of the loiter can be set via the sign: A positive value for +clockwise, negative for counter-clockwise. + -10000 + 10000 m 1 0.5 @@ -16763,7 +8981,11 @@ Minimum height above ground during Mission and RTL - Minimum height above ground the vehicle is allowed to descend to during Mission and RTL, excluding landing commands. Requires a distance sensor to be set up. Note: only prevents the vehicle from descending further, but does not force it to climb. Set to a negative value to disable. + Minimum height above ground the vehicle is allowed to descend to during Mission and RTL, +excluding landing commands. +Requires a distance sensor to be set up. +Note: only prevents the vehicle from descending further, but does not force it to climb. +Set to a negative value to disable. -1 m 1 @@ -16771,7 +8993,10 @@ Minimum Loiter altitude - This is the minimum altitude above Home the system will always obey in Loiter (Hold) mode if switched into this mode without specifying an altitude (e.g. through Loiter switch on RC). Doesn't affect Loiters that are part of Missions or that are entered through a reposition setpoint ("Go to"). Set to a negative value to disable. + This is the minimum altitude above Home the system will always obey in Loiter (Hold) mode if switched into this +mode without specifying an altitude (e.g. through Loiter switch on RC). +Doesn't affect Loiters that are part of Missions or that are entered through a reposition setpoint ("Go to"). +Set to a negative value to disable. -1 m 1 @@ -16779,7 +9004,8 @@ Set traffic avoidance mode - Enabling this will allow the system to respond to transponder data from e.g. ADSB transponders + Enabling this will allow the system to respond +to transponder data from e.g. ADSB transponders Disabled Warn only @@ -16802,7 +9028,8 @@ Estimated time until collision - Minimum acceptable time until collsion. Assumes constant speed over 3d distance. + Minimum acceptable time until collsion. +Assumes constant speed over 3d distance. 1 900000000 s @@ -16811,7 +9038,11 @@ Multicopter air-mode - The air-mode enables the mixer to increase the total thrust of the multirotor in order to keep attitude and rate control even at low and high throttle. This function should be disabled during tuning as it will help the controller to diverge if the closed-loop is unstable (i.e. the vehicle is not tuned yet). Enabling air-mode for yaw requires the use of an arming switch. + The air-mode enables the mixer to increase the total thrust of the multirotor +in order to keep attitude and rate control even at low and high throttle. +This function should be disabled during tuning as it will help the controller +to diverge if the closed-loop is unstable (i.e. the vehicle is not tuned yet). +Enabling air-mode for yaw requires the use of an arming switch. Disabled Roll/Pitch @@ -16819,23 +9050,17 @@ - - - Custom configuration for ModalAI drones - This can be set to indicate that drone behavior needs to be changed to match a custom setting - True - - Stabilize the mount - Set to true for servo gimbal, false for passthrough. This is required for a gimbal which is not capable of stabilizing itself and relies on the IMU's attitude estimation. - 0 - 2 + Set to true for servo gimbal, false for passthrough. +This is required for a gimbal which is not capable of stabilizing itself +and relies on the IMU's attitude estimation. Disable Stabilize all axis Stabilize yaw for absolute/lock mode. + Stabilize pitch for absolute/lock mode. @@ -16902,9 +9127,23 @@ Mavlink System ID of the mount If MNT_MODE_OUT is MAVLink gimbal protocol v1, mount configure/control commands will be sent with this target ID. + + Max positive angle of pitch setpoint (only in MNT_MODE_OUT=AUX) + Use output driver settings to calibrate (e.g. PWM_CENT/_MIN/_MAX). + deg + 1 + + + Min negative angle of pitch setpoint (only in MNT_MODE_OUT=AUX) + Use output driver settings to calibrate (e.g. PWM_CENT/_MIN/_MAX). + deg + 1 + Mount input mode - This is the protocol used between the ground station and the autopilot. Recommended is Auto, RC only or MAVLink gimbal protocol v2. The rest will be deprecated. + This is the protocol used between the ground station and the autopilot. +Recommended is Auto, RC only or MAVLink gimbal protocol v2. +The rest will be deprecated. -1 4 true @@ -16919,7 +9158,8 @@ Mount output mode - This is the protocol used between the autopilot and a connected gimbal. Recommended is the MAVLink gimbal protocol v2 if the gimbal supports it. + This is the protocol used between the autopilot and a connected gimbal. +Recommended is the MAVLink gimbal protocol v2 if the gimbal supports it. 0 2 true @@ -16929,43 +9169,17 @@ MAVLink gimbal protocol v2 - - Offset for pitch channel output in degrees - -360.0 - 360.0 - deg - 1 - - - Offset for roll channel output in degrees - -360.0 - 360.0 - deg - 1 - - - Offset for yaw channel output in degrees - -360.0 - 360.0 - deg - 1 - - - Range of pitch channel output in degrees (only in AUX output mode) - 1.0 - 720.0 - deg - 1 - - Range of roll channel output in degrees (only in AUX output mode) + Range of roll channel output (only in MNT_MODE_OUT=AUX) + Use output driver settings to calibrate (e.g. PWM_CENT/_MIN/_MAX). Note that only symmetric angular ranges are supported. 1.0 720.0 deg 1 - Range of yaw channel output in degrees (only in AUX output mode) + Range of yaw channel output (only in MNT_MODE_OUT=AUX) + Use output driver settings to calibrate (e.g. PWM_CENT/_MIN/_MAX). Note that only symmetric angular ranges are supported. 1.0 720.0 deg @@ -16994,18 +9208,29 @@ Angular rate + + Alpha filter time constant defining the convergence rate (in seconds) for open-loop AUX mount control + Use when no angular position feedback is available. +With MNT_MODE_OUT set to AUX, the mount operates in open-loop and directly commands the servo output. +Parameters must be tuned for the specific servo to approximate its speed and response. + 0.0 + Acro mode roll, pitch expo factor - Exponential factor for tuning the input curve shape. 0 Purely linear input curve 1 Purely cubic input curve + Exponential factor for tuning the input curve shape. +0 Purely linear input curve +1 Purely cubic input curve 0 1 2 Acro mode yaw expo factor - Exponential factor for tuning the input curve shape. 0 Purely linear input curve 1 Purely cubic input curve + Exponential factor for tuning the input curve shape. +0 Purely linear input curve +1 Purely cubic input curve 0 1 2 @@ -17030,14 +9255,20 @@ Acro mode roll, pitch super expo factor - "Superexponential" factor for refining the input curve shape tuned using MC_ACRO_EXPO. 0 Pure Expo function 0.7 reasonable shape enhancement for intuitive stick feel 0.95 very strong bent input curve only near maxima have effect + "Superexponential" factor for refining the input curve shape tuned using MC_ACRO_EXPO. +0 Pure Expo function +0.7 reasonable shape enhancement for intuitive stick feel +0.95 very strong bent input curve only near maxima have effect 0 0.95 2 Acro mode yaw super expo factor - "Superexponential" factor for refining the input curve shape tuned using MC_ACRO_EXPO_Y. 0 Pure Expo function 0.7 reasonable shape enhancement for intuitive stick feel 0.95 very strong bent input curve only near maxima have effect + "Superexponential" factor for refining the input curve shape tuned using MC_ACRO_EXPO_Y. +0 Pure Expo function +0.7 reasonable shape enhancement for intuitive stick feel +0.95 very strong bent input curve only near maxima have effect 0 0.95 2 @@ -17055,14 +9286,18 @@ Max pitch rate - Limit for pitch rate in manual and auto modes (except acro). Has effect for large rotations in autonomous mode, to avoid large control output and mixer saturation. This is not only limited by the vehicle's properties, but also by the maximum measurement rate of the gyro. + Limit for pitch rate in manual and auto modes (except acro). +Has effect for large rotations in autonomous mode, to avoid large control +output and mixer saturation. +This is not only limited by the vehicle's properties, but also by the maximum +measurement rate of the gyro. 0.0 1800.0 deg/s 1 5 - + Pitch P gain Pitch proportional gain, i.e. desired angular speed in rad/s for error 1 rad. 0.0 @@ -17072,14 +9307,18 @@ Max roll rate - Limit for roll rate in manual and auto modes (except acro). Has effect for large rotations in autonomous mode, to avoid large control output and mixer saturation. This is not only limited by the vehicle's properties, but also by the maximum measurement rate of the gyro. + Limit for roll rate in manual and auto modes (except acro). +Has effect for large rotations in autonomous mode, to avoid large control +output and mixer saturation. +This is not only limited by the vehicle's properties, but also by the maximum +measurement rate of the gyro. 0.0 1800.0 deg/s 1 5 - + Roll P gain Roll proportional gain, i.e. desired angular speed in rad/s for error 1 rad. 0.0 @@ -17105,24 +9344,30 @@ Yaw weight - A fraction [0,1] deprioritizing yaw compared to roll and pitch in non-linear attitude control. Deprioritizing yaw is necessary because multicopters have much less control authority in yaw compared to the other axes and it makes sense because yaw is not critical for stable hovering or 3D navigation. For yaw control tuning use MC_YAW_P. This ratio has no impact on the yaw gain. + A fraction [0,1] deprioritizing yaw compared to roll and pitch in non-linear attitude control. +Deprioritizing yaw is necessary because multicopters have much less control authority +in yaw compared to the other axes and it makes sense because yaw is not critical for +stable hovering or 3D navigation. +For yaw control tuning use MC_YAW_P. This ratio has no impact on the yaw gain. 0.0 1.0 2 0.1 - + Maximum yaw acceleration in autonomous modes - Limits the acceleration of the yaw setpoint to avoid large control output and mixer saturation. + Limits the acceleration of the yaw setpoint to avoid large +control output and mixer saturation. 5 360 deg/s^2 0 5 - + Maximum yaw rate in autonomous modes - Limits the rate of change of the yaw setpoint to avoid large control output and mixer saturation. + Limits the rate of change of the yaw setpoint to avoid large +control output and mixer saturation. 5 360 deg/s @@ -17166,7 +9411,9 @@ Acceleration to tilt coupling - Set to decouple tilt from vertical acceleration. This provides smoother flight but slightly worse tracking in position and auto modes. Unset if accurate position tracking during dynamic maneuvers is more important than a smooth flight. + Set to decouple tilt from vertical acceleration. +This provides smoother flight but slightly worse tracking in position and auto modes. +Unset if accurate position tracking during dynamic maneuvers is more important than a smooth flight. Maximum downwards acceleration in climb rate controlled modes @@ -17187,7 +9434,9 @@ Maximum horizontal acceleration - MPC_POS_MODE 1 just deceleration 4 not used, use MPC_ACC_HOR instead + MPC_POS_MODE +1 just deceleration +4 not used, use MPC_ACC_HOR instead 2 15 m/s^2 @@ -17204,7 +9453,13 @@ Altitude reference mode - Set to 0 to control height relative to the earth frame origin. This origin may move up and down in flight due to sensor drift. Set to 1 to control height relative to estimated distance to ground. The vehicle will move up and down with terrain height variation. Requires a distance to ground sensor. The height controller will revert to using height above origin if the distance to ground estimate becomes invalid as indicated by the local_position.distance_bottom_valid message being false. Set to 2 to control height relative to ground (requires a distance sensor) when stationary and relative to earth frame origin when moving horizontally. The speed threshold is controlled by the MPC_HOLD_MAX_XY parameter. + Control height +0: relative earth frame origin which may drift due to sensors +1: relative to ground (requires distance sensor) which changes with terrain variation. +It will revert to relative earth frame if the distance to ground estimate becomes invalid. +2: relative to ground (requires distance sensor) when stationary +and relative to earth frame when moving horizontally. +The speed threshold is MPC_HOLD_MAX_XY 0 2 @@ -17213,14 +9468,6 @@ Terrain hold - - Deadzone for sticks in manual piloted modes - Does not apply to manual throttle and direct attitude piloting by stick. - 0 - 1 - 2 - 0.01 - Maximum horizontal velocity for which position hold is enabled (use 0 to disable check) Only used with MPC_POS_MODE Direct velocity or MPC_ALT_MODE 2 @@ -17239,7 +9486,8 @@ Jerk limit in autonomous modes - Limit the maximum jerk of the vehicle (how fast the acceleration can change). A lower value leads to smoother vehicle motions but also limited agility. + Limit the maximum jerk of the vehicle (how fast the acceleration can change). +A lower value leads to smoother vehicle motions but also limited agility. 1 80 m/s^3 @@ -17248,7 +9496,10 @@ Maximum horizontal and vertical jerk in Position/Altitude mode - Limit the maximum jerk of the vehicle (how fast the acceleration can change). A lower value leads to smoother motions but limits agility (how fast it can change directions or break). Setting this to the maximum value essentially disables the limit. Only used with MPC_POS_MODE Acceleration based. + Limit the maximum jerk (acceleration change) of the vehicle. +A lower value leads to smoother motions but limits agility. +Setting this to the maximum value essentially disables the limit. +Only used with MPC_POS_MODE Acceleration based. 0.5 500 m/s^3 @@ -17257,7 +9508,9 @@ Altitude for 1. step of slow landing (descend) - Below this altitude descending velocity gets limited to a value between "MPC_Z_VEL_MAX_DN" (or "MPC_Z_V_AUTO_DN") and "MPC_LAND_SPEED" Value needs to be higher than "MPC_LAND_ALT2" + Below this altitude descending velocity gets limited to a value +between "MPC_Z_VEL_MAX_DN" (or "MPC_Z_V_AUTO_DN") and "MPC_LAND_SPEED" +Value needs to be higher than "MPC_LAND_ALT2" 0 122 m @@ -17265,7 +9518,9 @@ Altitude for 2. step of slow landing (landing) - Below this altitude descending velocity gets limited to "MPC_LAND_SPEED" Value needs to be lower than "MPC_LAND_ALT1" + Below this altitude descending velocity gets +limited to "MPC_LAND_SPEED" +Value needs to be lower than "MPC_LAND_ALT1" 0 122 m @@ -17273,7 +9528,8 @@ Altitude for 3. step of slow landing - Below this altitude descending velocity gets limited to "MPC_LAND_CRWL", if LIDAR available. No effect if LIDAR not available + If a valid distance sensor measurement to the ground is available, +limit descending velocity to "MPC_LAND_CRWL" below this altitude. 0 122 m @@ -17286,17 +9542,26 @@ m/s 1 - + User assisted landing radius - When nudging is enabled (see MPC_LAND_RC_HELP), this controls the maximum allowed horizontal displacement from the original landing point. - 0 + When nudging is enabled (see MPC_LAND_RC_HELP), this defines the maximum +allowed horizontal displacement from the original landing point. +- If inside of the radius, only allow nudging inputs that do not move the vehicle outside of it. +- If outside of the radius, only allow nudging inputs that move the vehicle back towards it. +Set it to -1 for infinite radius. + -1 m 0 1 Enable nudging based on user input during autonomous land routine - Using stick input the vehicle can be moved horizontally and yawed. The descend speed is amended: stick full up - 0 stick centered - MPC_LAND_SPEED stick full down - 2 * MPC_LAND_SPEED Manual override during auto modes has to be disabled to use this feature (see COM_RC_OVERRIDE). + Using stick input the vehicle can be moved horizontally and yawed. +The descend speed is amended: +stick full up - 0 +stick centered - MPC_LAND_SPEED +stick full down - 2 * MPC_LAND_SPEED +Manual override during auto modes has to be disabled to use this feature (see COM_RC_OVERRIDE). 0 1 @@ -17312,7 +9577,9 @@ Minimum collective thrust in Stabilized mode - The value is mapped to the lowest throttle stick position in Stabilized mode. Too low collective thrust leads to loss of roll/pitch/yaw torque control authority. Airmode is used to keep torque authority with zero thrust (see MC_AIRMODE). + The value is mapped to the lowest throttle stick position in Stabilized mode. +Too low collective thrust leads to loss of roll/pitch/yaw torque control authority. +Airmode is used to keep torque authority with zero thrust (see MC_AIRMODE). 0 1 norm @@ -17320,7 +9587,7 @@ 0.01 - Maximal tilt angle in Stabilized or Altitude mode + Maximal tilt angle in Stabilized, Altitude and Altitude Cruise mode 1 70 deg @@ -17337,7 +9604,8 @@ Manual yaw rate input filter time constant - Not used in Stabilized mode Setting this parameter to 0 disables the filter + Not used in Stabilized mode +Setting this parameter to 0 disables the filter 0 5 s @@ -17346,7 +9614,13 @@ Position/Altitude mode variant - The supported sub-modes are: - "Direct velocity": Sticks directly map to velocity setpoints without smoothing. Also applies to vertical direction and Altitude mode. Useful for velocity control tuning. - "Acceleration based": Sticks map to acceleration and there's a virtual brake drag + The supported sub-modes are: +Direct velocity: +Sticks directly map to velocity setpoints without smoothing. +Also applies to vertical direction and Altitude mode. +Useful for velocity control tuning. +Acceleration based: +Sticks map to acceleration and there's a virtual brake drag Direct velocity Acceleration based @@ -17354,15 +9628,27 @@ Thrust curve mapping in Stabilized Mode - This parameter defines how the throttle stick input is mapped to collective thrust in Stabilized mode. In case the default is used ('Rescale to hover thrust'), the stick input is linearly rescaled, such that a centered stick corresponds to the hover throttle (see MPC_THR_HOVER). Select 'No Rescale' to directly map the stick 1:1 to the output. This can be useful in case the hover thrust is very low and the default would lead to too much distortion (e.g. if hover thrust is set to 20%, then 80% of the upper thrust range is squeezed into the upper half of the stick range). Note: In case MPC_THR_HOVER is set to 50%, the modes 0 and 1 are the same. + Defines how the throttle stick is mapped to collective thrust in Stabilized mode. +Rescale to hover thrust estimate: +Stick input is linearly rescaled, such that a centered throttle stick corresponds to the hover thrust estimator's output. +No rescale: +Directly map the stick 1:1 to the output. +Can be useful with very low hover thrust which leads to much distortion and the upper half getting sensitive. +Rescale to hover thrust parameter: +Similar to rescaling to the hover thrust estimate, but it uses the hover thrust parameter value (see MPC_THR_HOVER) instead of estimated value. +With MPC_THR_HOVER 0.5 it's equivalent to No rescale. - Rescale to hover thrust - No Rescale + Rescale to estimate + No rescale + Rescale to parameter Vertical thrust required to hover - Mapped to center throttle stick in Stabilized mode (see MPC_THR_CURVE). Used for initialization of the hover thrust estimator (see MPC_USE_HTE). The estimated hover thrust is used as base for zero vertical acceleration in altitude control. The hover thrust is important for land detection to work correctly. + Mapped to center throttle stick in Stabilized mode (see MPC_THR_CURVE). +Used for initialization of the hover thrust estimator. +The estimated hover thrust is used as base for zero vertical acceleration in altitude control. +The hover thrust is important for land detection to work correctly. 0.1 0.8 norm @@ -17380,7 +9666,9 @@ Minimum collective thrust in climb rate controlled modes - Too low thrust leads to loss of roll/pitch/yaw torque control authority. With airmode enabled this parameters can be set to 0 while still keeping torque authority (see MC_AIRMODE). + Too low thrust leads to loss of roll/pitch/yaw torque control authority. +With airmode enabled this parameters can be set to 0 +while still keeping torque authority (see MC_AIRMODE). 0.05 0.5 norm @@ -17389,7 +9677,8 @@ Horizontal thrust margin - Margin that is kept for horizontal control when higher priority vertical thrust is saturated. To avoid completely starving horizontal control with high vertical error. + Margin that is kept for horizontal control when higher priority vertical thrust is saturated. +To avoid completely starving horizontal control with high vertical error. 0 0.5 norm @@ -17398,7 +9687,8 @@ Maximum tilt angle in air - Absolute maximum for all velocity or acceleration controlled modes. Any higher value is truncated. + Absolute maximum for all velocity or acceleration controlled modes. +Any higher value is truncated. 20 89 deg @@ -17416,7 +9706,9 @@ Smooth takeoff ramp time constant - Increasing this value will make climb rate controlled takeoff slower. If it's too slow the drone might scratch the ground and tip over. A time constant of 0 disables the ramp + Increasing this value will make climb rate controlled takeoff slower. +If it's too slow the drone might scratch the ground and tip over. +A time constant of 0 disables the ramp 0 5 s @@ -17428,10 +9720,6 @@ m/s 2 - - Hover thrust estimator - Disable to use the fixed parameter MPC_THR_HOVER Enable to use the hover thrust estimator - Velocity derivative low pass cutoff frequency A value of 0 disables the filter. @@ -17452,7 +9740,9 @@ Maximum horizontal velocity setpoint in Position mode - Must be smaller than MPC_XY_VEL_MAX. The maximum sideways and backward speed can be set differently using MPC_VEL_MAN_SIDE and MPC_VEL_MAN_BACK, respectively. + Must be smaller than MPC_XY_VEL_MAX. +The maximum sideways and backward speed can be set differently +using MPC_VEL_MAN_SIDE and MPC_VEL_MAN_BACK, respectively. 3 20 m/s @@ -17461,7 +9751,8 @@ Maximum backward velocity in Position mode - If set to a negative value or larger than MPC_VEL_MANUAL then MPC_VEL_MANUAL is used. + If set to a negative value or larger than +MPC_VEL_MANUAL then MPC_VEL_MANUAL is used. -1 20 m/s @@ -17470,7 +9761,8 @@ Maximum sideways velocity in Position mode - If set to a negative value or larger than MPC_VEL_MANUAL then MPC_VEL_MANUAL is used. + If set to a negative value or larger than +MPC_VEL_MANUAL then MPC_VEL_MANUAL is used. -1 20 m/s @@ -17488,7 +9780,8 @@ Velocity notch filter frequency - The center frequency for the 2nd order notch filter on the velocity. A value of 0 disables the filter. + The center frequency for the 2nd order notch filter on the velocity. +A value of 0 disables the filter. 0 50 Hz @@ -17506,20 +9799,17 @@ Maximum horizontal error allowed by the trajectory generator - The integration speed of the trajectory setpoint is linearly reduced with the horizontal position tracking error. When the error is above this parameter, the integration of the trajectory is stopped to wait for the drone. This value can be adjusted depending on the tracking capabilities of the vehicle. + The integration speed of the trajectory setpoint is linearly +reduced with the horizontal position tracking error. When the +error is above this parameter, the integration of the +trajectory is stopped to wait for the drone. +This value can be adjusted depending on the tracking +capabilities of the vehicle. 0.1 10 1 1 - - Manual position control stick exponential curve sensitivity - The higher the value the less sensitivity the stick has around zero while still reaching the maximum value with full stick deflection. 0 Purely linear input curve 1 Purely cubic input curve - 0 - 1 - 2 - 0.01 - Proportional gain for horizontal position error Defined as corrective velocity in m/s per m position error @@ -17537,7 +9827,9 @@ Overall Horizontal Velocity Limit - If set to a value greater than zero, other parameters are automatically set (such as MPC_XY_VEL_MAX or MPC_VEL_MANUAL). If set to a negative value, the existing individual parameters are used. + If set to a value greater than zero, other parameters are automatically set (such as +MPC_XY_VEL_MAX or MPC_VEL_MANUAL). +If set to a negative value, the existing individual parameters are used. -20 20 1 @@ -17553,7 +9845,8 @@ Integral gain for horizontal velocity error - Defined as correction acceleration in m/s^2 per m velocity integral Allows to eliminate steady state errors in disturbances like wind. + Defined as correction acceleration in m/s^2 per m velocity integral +Allows to eliminate steady state errors in disturbances like wind. 0 60 2 @@ -17561,7 +9854,8 @@ Maximum horizontal velocity - Absolute maximum for all velocity controlled modes. Any higher value is truncated. + Absolute maximum for all velocity controlled modes. +Any higher value is truncated. 0 20 m/s @@ -17576,22 +9870,6 @@ 2 0.1 - - Manual control stick yaw rotation exponential curve - The higher the value the less sensitivity the stick has around zero while still reaching the maximum value with full stick deflection. 0 Purely linear input curve 1 Purely cubic input curve - 0 - 1 - 2 - 0.01 - - - Manual control stick vertical exponential curve - The higher the value the less sensitivity the stick has around zero while still reaching the maximum value with full stick deflection. 0 Purely linear input curve 1 Purely cubic input curve - 0 - 1 - 2 - 0.01 - Proportional gain for vertical position error Defined as corrective velocity in m/s per m position error @@ -17602,7 +9880,9 @@ Overall Vertical Velocity Limit - If set to a value greater than zero, other parameters are automatically set (such as MPC_Z_VEL_MAX_UP or MPC_LAND_SPEED). If set to a negative value, the existing individual parameters are used. + If set to a value greater than zero, other parameters are automatically set (such as +MPC_Z_VEL_MAX_UP or MPC_LAND_SPEED). +If set to a negative value, the existing individual parameters are used. -3 8 1 @@ -17626,7 +9906,9 @@ Maximum descent velocity - Absolute maximum for all climb rate controlled modes. In manually piloted modes full stick deflection commands this velocity. For default autonomous velocity see MPC_Z_V_AUTO_UP + Absolute maximum for all climb rate controlled modes. +In manually piloted modes full stick deflection commands this velocity. +For default autonomous velocity see MPC_Z_V_AUTO_UP 0.5 4 m/s @@ -17635,7 +9917,9 @@ Maximum ascent velocity - Absolute maximum for all climb rate controlled modes. In manually piloted modes full stick deflection commands this velocity. For default autonomous velocity see MPC_Z_V_AUTO_UP + Absolute maximum for all climb rate controlled modes. +In manually piloted modes full stick deflection commands this velocity. +For default autonomous velocity see MPC_Z_V_AUTO_UP 0.5 8 m/s @@ -17670,7 +9954,11 @@ Responsiveness - Changes the overall responsiveness of the vehicle. The higher the value, the faster the vehicle will react. If set to a value greater than zero, other parameters are automatically set (such as the acceleration or jerk limits). If set to a negative value, the existing individual parameters are used. + Changes the overall responsiveness of the vehicle. +The higher the value, the faster the vehicle will react. +If set to a value greater than zero, other parameters are automatically set (such as +the acceleration or jerk limits). +If set to a negative value, the existing individual parameters are used. -1 1 2 @@ -17695,7 +9983,9 @@ Default horizontal velocity limit - This value is used in slow mode if no aux channel is mapped and no limit is commanded through MAVLink. + This value is used in slow mode if +no aux channel is mapped and +no limit is commanded through MAVLink. 0.1 m/s 2 @@ -17703,7 +9993,9 @@ Default vertical velocity limit - This value is used in slow mode if no aux channel is mapped and no limit is commanded through MAVLink. + This value is used in slow mode if +no aux channel is mapped and +no limit is commanded through MAVLink. 0.1 m/s 2 @@ -17711,7 +10003,9 @@ Default yaw rate limit - This value is used in slow mode if no aux channel is mapped and no limit is commanded through MAVLink. + This value is used in slow mode if +no aux channel is mapped and +no limit is commanded through MAVLink. 1 deg/s 0 @@ -17729,6 +10023,18 @@ AUX6 + + RC_MAP_AUX{N} to allow for gimbal pitch rate control in position slow mode + + No pitch rate input + AUX1 + AUX2 + AUX3 + AUX4 + AUX5 + AUX6 + + Manual input mapped to scale vertical velocity in position slow mode @@ -17781,7 +10087,11 @@ Battery power level scaler - This compensates for voltage drop of the battery over time by attempting to normalize performance across the operating range of the battery. The copter should constantly behave as if it was fully charged with reduced max acceleration at lower battery percentages. i.e. if hover is at 0.5 throttle at 100% battery, it will still be 0.5 at 60% battery. + This compensates for voltage drop of the battery over time by attempting to +normalize performance across the operating range of the battery. The copter +should constantly behave as if it was fully charged with reduced max acceleration +at lower battery percentages. i.e. if hover is at 0.5 throttle at 100% battery, +it will still be 0.5 at 60% battery. Pitch rate D gain @@ -17805,7 +10115,13 @@ Pitch rate controller gain - Global gain of the controller. This gain scales the P, I and D terms of the controller: output = MC_PITCHRATE_K * (MC_PITCHRATE_P * error + MC_PITCHRATE_I * error_integral + MC_PITCHRATE_D * error_derivative) Set MC_PITCHRATE_P=1 to implement a PID in the ideal form. Set MC_PITCHRATE_K=1 to implement a PID in the parallel form. + Global gain of the controller. +This gain scales the P, I and D terms of the controller: +output = MC_PITCHRATE_K * (MC_PITCHRATE_P * error ++ MC_PITCHRATE_I * error_integral ++ MC_PITCHRATE_D * error_derivative) +Set MC_PITCHRATE_P=1 to implement a PID in the ideal form. +Set MC_PITCHRATE_K=1 to implement a PID in the parallel form. 0.01 5.0 4 @@ -17849,7 +10165,13 @@ Roll rate controller gain - Global gain of the controller. This gain scales the P, I and D terms of the controller: output = MC_ROLLRATE_K * (MC_ROLLRATE_P * error + MC_ROLLRATE_I * error_integral + MC_ROLLRATE_D * error_derivative) Set MC_ROLLRATE_P=1 to implement a PID in the ideal form. Set MC_ROLLRATE_K=1 to implement a PID in the parallel form. + Global gain of the controller. +This gain scales the P, I and D terms of the controller: +output = MC_ROLLRATE_K * (MC_ROLLRATE_P * error ++ MC_ROLLRATE_I * error_integral ++ MC_ROLLRATE_D * error_derivative) +Set MC_ROLLRATE_P=1 to implement a PID in the ideal form. +Set MC_ROLLRATE_K=1 to implement a PID in the parallel form. 0.01 5.0 4 @@ -17874,27 +10196,32 @@ Yaw rate D gain Yaw rate differential gain. Small values help reduce fast oscillations. If value is too big oscillations will appear again. 0.0 - 2 - 0.01 + 4 + 0.0005 Yaw rate feedforward Improves tracking performance. 0.0 4 - 0.01 Yaw rate I gain Yaw rate integral gain. Can be set to compensate static thrust difference or gravity center offset. 0.0 - 2 + 3 0.01 Yaw rate controller gain - Global gain of the controller. This gain scales the P, I and D terms of the controller: output = MC_YAWRATE_K * (MC_YAWRATE_P * error + MC_YAWRATE_I * error_integral + MC_YAWRATE_D * error_derivative) Set MC_YAWRATE_P=1 to implement a PID in the ideal form. Set MC_YAWRATE_K=1 to implement a PID in the parallel form. - 0.0 + Global gain of the controller. +This gain scales the P, I and D terms of the controller: +output = MC_YAWRATE_K * (MC_YAWRATE_P * error ++ MC_YAWRATE_I * error_integral ++ MC_YAWRATE_D * error_derivative) +Set MC_YAWRATE_P=1 to implement a PID in the ideal form. +Set MC_YAWRATE_K=1 to implement a PID in the parallel form. + 0.01 5.0 4 0.0005 @@ -17904,9 +10231,18 @@ Yaw rate proportional gain, i.e. control output for angular speed error 1 rad/s. 0.0 0.6 - 2 + 3 0.01 + + Low pass filter cutoff frequency for yaw torque setpoint + Reduces vibrations by lowering high frequency torque caused by rotor acceleration. +0 disables the filter + 0 + 10 + Hz + 3 + Yaw rate integrator limit Yaw rate integrator limit. Can be set to increase the amount of integrator available to counteract disturbances or reduced to improve settling time after large yaw moment trim changes. @@ -17916,38 +10252,11 @@ - - Serial Configuration for MSP OSD - Configure on which serial port to run MSP OSD. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Enable/Disable the ATXXX OSD Chip - Configure the ATXXXX OSD Chip (mounted on the OmnibusF4SD board) and select the transmission standard. - true - - Disabled - NTSC - PAL - - OSD Crosshairs Height - Controls the vertical position of the crosshair display. Resolution is limited by OSD to 15 discrete values. Negative values will display the crosshairs below the horizon + Controls the vertical position of the crosshair display. +Resolution is limited by OSD to 15 discrete values. Negative +values will display the crosshairs below the horizon -8 8 @@ -17961,9 +10270,16 @@ OSD Warning Level Minimum security of log level to display on the OSD. + + OSD RC Stick commands + Forward RC stick input to VTX when disarmed + 0 + 1 + OSD Scroll Rate (ms) - Scroll rate in milliseconds for OSD messages longer than available character width. This is lower-bounded by the nominal loop rate of this module. + Scroll rate in milliseconds for OSD messages longer than available character width. +This is lower-bounded by the nominal loop rate of this module. 100 1000 @@ -17999,13 +10315,13 @@ - - S.BUS out - Set to 1 to enable S.BUS version 1 output instead of RSSI. - Thrust to motor control signal model parameter - Parameter used to model the nonlinear relationship between motor control signal (e.g. PWM) and static thrust. The model is: rel_thrust = factor * rel_signal^2 + (1-factor) * rel_signal, where rel_thrust is the normalized thrust between 0 and 1, and rel_signal is the relative motor control signal between 0 and 1. + Parameter used to model the nonlinear relationship between +motor control signal (e.g. PWM) and static thrust. +The model is: rel_thrust = factor * rel_signal^2 + (1-factor) * rel_signal, +where rel_thrust is the normalized thrust between 0 and 1, and +rel_signal is the relative motor control signal between 0 and 1. 0.0 1.0 1 @@ -18013,13 +10329,12 @@ - - Enable Gripper actuation in Payload Deliverer - True - - + Timeout for successful gripper actuation acknowledgement - Maximum time Gripper will wait while the successful griper actuation isn't recognised. If the gripper has no feedback sensor, it will simply wait for this time before considering gripper actuation successful and publish a 'VehicleCommandAck' signaling successful gripper action + Maximum time Gripper will wait while the successful griper actuation isn't recognised. +If the gripper has no feedback sensor, it will simply wait for +this time before considering gripper actuation successful and publish a +'VehicleCommandAck' signaling successful gripper action 0 s @@ -18087,7 +10402,7 @@ - + Tuning parameter for the pure pursuit controller Lower value -> More aggressive controller (beware overshoot/oscillations) 0.1 @@ -18095,7 +10410,7 @@ 2 0.01 - + Maximum lookahead distance for the pure pursuit controller 0.1 100 @@ -18103,7 +10418,7 @@ 2 0.01 - + Minimum lookahead distance for the pure pursuit controller 0.1 100 @@ -18112,42 +10427,7 @@ 0.01 - - - Crossfire RC telemetry enable - Crossfire telemetry enable - - - Ghost RC telemetry enable - Ghost telemetry enable - - - - - RC input protocol - Select your RC input protocol or auto to scan. - -1 - 7 - - Auto - None - PPM - SBUS - DSM - ST24 - SUMD - CRSF - GHST - - - - - RC channel 10 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 10 maximum Maximum value for this channel. @@ -18179,12 +10459,6 @@ 2200.0 us - - RC channel 11 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 11 maximum Maximum value for this channel. @@ -18216,12 +10490,6 @@ 2200.0 us - - RC channel 12 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 12 maximum Maximum value for this channel. @@ -18253,12 +10521,6 @@ 2200.0 us - - RC channel 13 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 13 maximum Maximum value for this channel. @@ -18290,12 +10552,6 @@ 2200.0 us - - RC channel 14 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 14 maximum Maximum value for this channel. @@ -18327,12 +10583,6 @@ 2200.0 us - - RC channel 15 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 15 maximum Maximum value for this channel. @@ -18364,12 +10614,6 @@ 2200.0 us - - RC channel 16 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 16 maximum Maximum value for this channel. @@ -18401,12 +10645,6 @@ 2200.0 us - - RC channel 17 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 17 maximum Maximum value for this channel. @@ -18438,12 +10676,6 @@ 2200.0 us - - RC channel 18 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 18 maximum Maximum value for this channel. @@ -18475,13 +10707,6 @@ 2200.0 us - - RC channel 1 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - us - RC channel 1 maximum Maximum value for RC channel 1 @@ -18513,13 +10738,6 @@ 2200.0 us - - RC channel 2 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - us - RC channel 2 maximum Maximum value for this channel. @@ -18551,13 +10769,6 @@ 2200.0 us - - RC channel 3 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - us - RC channel 3 maximum Maximum value for this channel. @@ -18589,13 +10800,6 @@ 2200.0 us - - RC channel 4 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - us - RC channel 4 maximum Maximum value for this channel. @@ -18627,12 +10831,6 @@ 2200.0 us - - RC channel 5 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 5 maximum Maximum value for this channel. @@ -18664,12 +10862,6 @@ 2200.0 us - - RC channel 6 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 6 maximum Maximum value for this channel. @@ -18701,12 +10893,6 @@ 2200.0 us - - RC channel 7 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 7 maximum Maximum value for this channel. @@ -18738,12 +10924,6 @@ 2200.0 us - - RC channel 8 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 8 maximum Maximum value for this channel. @@ -18775,12 +10955,6 @@ 2200.0 us - - RC channel 9 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 9 maximum Maximum value for this channel. @@ -18814,20 +10988,25 @@ RC channel count - This parameter is used by Ground Station software to save the number of channels which were used during RC calibration. It is only meant for ground station use. + This parameter is used by Ground Station software to save the number +of channels which were used during RC calibration. It is only meant +for ground station use. 0 18 Failsafe channel PWM threshold - Use RC_MAP_FAILSAFE to specify which channel is used to indicate RC loss via this threshold. By default this is the throttle channel. Set to a PWM value slightly above the PWM value for the channel (e.g. throttle) in a failsafe event, but below the minimum PWM value for the channel during normal operation. Note: The default value of 0 disables the feature (it is below the expected range). + Use RC_MAP_FAILSAFE to specify which channel is used to indicate RC loss via this threshold. +By default this is the throttle channel. +Set to a PWM value slightly above the PWM value for the channel (e.g. throttle) in a failsafe event, +but below the minimum PWM value for the channel during normal operation. +Note: The default value of 0 disables the feature (it is below the expected range). 0 2200 us AUX1 Passthrough RC channel - Default function: Camera pitch 0 18 @@ -18854,7 +11033,6 @@ AUX2 Passthrough RC channel - Default function: Camera roll 0 18 @@ -18881,7 +11059,6 @@ AUX3 Passthrough RC channel - Default function: Camera azimuth / yaw 0 18 @@ -19012,7 +11189,11 @@ Failsafe channel mapping - Configures which RC channel is used by the receiver to indicate the signal was lost (on receivers that use output a fixed signal value to report lost signal). If set to 0, the channel mapped to throttle is used. Use RC_FAILS_THR to set the threshold indicating lost signal. By default it's below the expected range and hence disabled. + Configures which RC channel is used by the receiver to indicate the signal was lost +(on receivers that use output a fixed signal value to report lost signal). +If set to 0, the channel mapped to throttle is used. +Use RC_FAILS_THR to set the threshold indicating lost signal. By default it's below +the expected range and hence disabled. 0 18 @@ -19039,7 +11220,8 @@ PARAM1 tuning channel - Can be used for parameter tuning with the RC. This one is further referenced as the 1st parameter channel. Set to 0 to deactivate * + Can be used for parameter tuning with the RC. This one is further referenced as the 1st parameter channel. +Set to 0 to deactivate * 0 18 @@ -19066,7 +11248,8 @@ PARAM2 tuning channel - Can be used for parameter tuning with the RC. This one is further referenced as the 2nd parameter channel. Set to 0 to deactivate * + Can be used for parameter tuning with the RC. This one is further referenced as the 2nd parameter channel. +Set to 0 to deactivate * 0 18 @@ -19093,7 +11276,8 @@ PARAM3 tuning channel - Can be used for parameter tuning with the RC. This one is further referenced as the 3th parameter channel. Set to 0 to deactivate * + Can be used for parameter tuning with the RC. This one is further referenced as the 3th parameter channel. +Set to 0 to deactivate * 0 18 @@ -19120,7 +11304,9 @@ Pitch control channel mapping - The channel index (starting from 1 for channel 1) indicates which channel should be used for reading pitch inputs from. A value of zero indicates the switch is not assigned. + The channel index (starting from 1 for channel 1) indicates +which channel should be used for reading pitch inputs from. +A value of zero indicates the switch is not assigned. 0 18 @@ -19147,7 +11333,9 @@ Roll control channel mapping - The channel index (starting from 1 for channel 1) indicates which channel should be used for reading roll inputs from. A value of zero indicates the switch is not assigned. + The channel index (starting from 1 for channel 1) indicates +which channel should be used for reading roll inputs from. +A value of zero indicates the switch is not assigned. 0 18 @@ -19174,7 +11362,9 @@ Throttle control channel mapping - The channel index (starting from 1 for channel 1) indicates which channel should be used for reading throttle inputs from. A value of zero indicates the switch is not assigned. + The channel index (starting from 1 for channel 1) indicates +which channel should be used for reading throttle inputs from. +A value of zero indicates the switch is not assigned. 0 18 @@ -19201,7 +11391,9 @@ Yaw control channel mapping - The channel index (starting from 1 for channel 1) indicates which channel should be used for reading yaw inputs from. A value of zero indicates the switch is not assigned. + The channel index (starting from 1 for channel 1) indicates +which channel should be used for reading yaw inputs from. +A value of zero indicates the switch is not assigned. 0 18 @@ -19228,7 +11420,9 @@ PWM input channel that provides RSSI - 0: do not read RSSI from input channel 1-18: read RSSI from specified input channel Specify the range for RSSI input with RC_RSSI_PWM_MIN and RC_RSSI_PWM_MAX parameters. + 0: do not read RSSI from input channel +1-18: read RSSI from specified input channel +Specify the range for RSSI input with RC_RSSI_PWM_MIN and RC_RSSI_PWM_MAX parameters. 0 18 @@ -19267,7 +11461,8 @@ Pitch trim - The trim value is the actuator control value the system needs for straight and level flight. + The trim value is the actuator control value the system needs +for straight and level flight. -0.5 0.5 2 @@ -19275,7 +11470,8 @@ Roll trim - The trim value is the actuator control value the system needs for straight and level flight. + The trim value is the actuator control value the system needs +for straight and level flight. -0.5 0.5 2 @@ -19283,7 +11479,8 @@ Yaw trim - The trim value is the actuator control value the system needs for straight and level flight. + The trim value is the actuator control value the system needs +for straight and level flight. -0.5 0.5 2 @@ -19293,37 +11490,68 @@ Threshold for the arm switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for selecting main motor engage - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for the landing gear switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for the kill switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for selecting loiter mode - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Arm switch channel - Use it to arm/disarm via switch instead of default throttle stick. If this is assigned, arming and disarming via stick is disabled. + Use it to arm/disarm via switch instead of default throttle stick. If this is +assigned, arming and disarming via stick is disabled. 0 18 @@ -19376,7 +11604,8 @@ Single channel flight mode selection - If this parameter is non-zero, flight modes are only selected by this channel and are assigned to six slots. + If this parameter is non-zero, flight modes are only selected +by this channel and are assigned to six slots. 0 18 @@ -19403,7 +11632,11 @@ Button flight mode selection - This bitmask allows to specify multiple channels for changing flight modes using momentary buttons. Each channel is assigned to a mode slot ((lowest channel = slot 1). The resulting modes for each slot X is defined by the COM_FLTMODEX parameters. The functionality can be used only if RC_MAP_FLTMODE is disabled. The maximum number of available slots and hence bits set in the mask is 6. + This bitmask allows to specify multiple channels for changing flight modes using +momentary buttons. Each channel is assigned to a mode slot ((lowest channel = slot 1). +The resulting modes for each slot X is defined by the COM_FLTMODEX parameters. +The functionality can be used only if RC_MAP_FLTMODE is disabled. +The maximum number of available slots and hence bits set in the mask is 6. 0 258048 @@ -19455,6 +11688,9 @@ Emergency Kill switch channel + This channel immediately sets all outputs to their disarmed values, parachutes are NOT deployed. +Unlike termination this can be undone. Quickly flipping the switch back restores control. +System auto-disarms after COM_KILL_DISARM seconds, preflight checks and re-arming are then required. 0 18 @@ -19507,7 +11743,10 @@ Mode switch channel mapping (deprecated) - This is the main flight mode selector. The channel index (starting from 1 for channel 1) indicates which channel should be used for deciding about the main mode. A value of zero indicates the switch is not assigned. + This is the main flight mode selector. +The channel index (starting from 1 for channel 1) indicates +which channel should be used for deciding about the main mode. +A value of zero indicates the switch is not assigned. 0 18 @@ -19610,6 +11849,36 @@ Channel 18 + + Termination switch channel + This channel triggers irreversible flight termination: +All outputs are disabled and set to their failsafe values (disarmed by default) +and MAVLink parachutes are triggered. +Unlike a kill switch, this cannot be undone until system reboot. Use with caution. + 0 + 18 + + Unassigned + Channel 1 + Channel 2 + Channel 3 + Channel 4 + Channel 5 + Channel 6 + Channel 7 + Channel 8 + Channel 9 + Channel 10 + Channel 11 + Channel 12 + Channel 13 + Channel 14 + Channel 15 + Channel 16 + Channel 17 + Channel 18 + + VTOL transition switch channel mapping 0 @@ -19638,33 +11907,70 @@ Threshold for selecting offboard mode - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th + -1 + 1 + 2 + + + Threshold for mid position of payload power switch + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 - Threshold for selecting payload power switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + Threshold for on position of payload power switch + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for selecting return to launch mode - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for the VTOL transition switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Half-angle of the return mode altitude cone - Defines the half-angle of a cone centered around the destination position that affects the altitude at which the vehicle returns. + Defines the half-angle of a cone centered around the destination position that +affects the altitude at which the vehicle returns. 0 90 deg @@ -19679,7 +11985,9 @@ Return mode loiter altitude - Descend to this altitude (above destination position) after return, and wait for time defined in RTL_LAND_DELAY. Land (i.e. slowly descend) from this altitude if autolanding allowed. VTOLs do transition to hover in this altitdue above the landing point. + Descend to this altitude (above destination position) after return, and wait for time defined in RTL_LAND_DELAY. +Land (i.e. slowly descend) from this altitude if autolanding allowed. +VTOLs do transition to hover in this altitdue above the landing point. 0 m 1 @@ -19687,7 +11995,8 @@ Return mode delay - Delay before landing (after initial descent) in Return mode. If set to -1 the system will not land but loiter at RTL_DESCEND_ALT. + Delay before landing (after initial descent) in Return mode. +If set to -1 the system will not land but loiter at RTL_DESCEND_ALT. -1 s 1 @@ -19703,7 +12012,8 @@ Horizontal radius from return point within which special rules for return mode apply - The return altitude will be calculated based on RTL_CONE_ANG parameter. The yaw setpoint will switch to the one defined by corresponding waypoint. + The return altitude will be calculated based on RTL_CONE_ANG parameter. +The yaw setpoint will switch to the one defined by corresponding waypoint. 0.5 m 1 @@ -19711,7 +12021,8 @@ RTL precision land mode - Use precision landing when doing an RTL landing phase. + Use precision landing when doing an RTL landing phase. +This setting does not apply for RTL destinations planned as part of a mission. No precision landing Opportunistic precision landing @@ -19719,316 +12030,119 @@ - Return mode return altitude - Default minimum altitude above destination (e.g. home, safe point, landing pattern) for return flight. The vehicle will climb to this altitude when Return mode is enganged, unless it currently is flying higher already. This is affected by RTL_MIN_DIST and RTL_CONE_ANG. - 0 - m - 1 - 0.5 - - - Return type - Return mode destination and flight path (home location, rally point, mission landing pattern, reverse mission) - - Return to closest safe point (home or rally point) via direct path. - Return to closest safe point other than home (mission landing pattern or rally point), via direct path. If no mission landing or rally points are defined return home via direct path. Always chose closest safe landing point if vehicle is a VTOL in hover mode. - Return to a planned mission landing, if available, using the mission path, else return to home via the reverse mission path. Do not consider rally points. - Return via direct path to closest destination: home, start of mission landing pattern or safe point. If the destination is a mission landing pattern, follow the pattern to land. - - - - - - RTL force approach landing - Only consider RTL point, if it has an approach defined. - - - RTL time estimate safety margin factor - Safety factor that is used to scale the actual RTL time estimate. Time with margin = RTL_TIME_FACTOR * time + RTL_TIME_MARGIN - 1.0 - 2.0 - 1 - 0.1 - - - RTL time estimate safety margin offset - Margin that is added to the time estimate, after it has already been scaled Time with margin = RTL_TIME_FACTOR * time + RTL_TIME_MARGIN - 0 - 3600 - s - 1 - 1 - - - - - Serial Configuration for Roboclaw Driver - Configure on which serial port to run Roboclaw Driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - - - Address of the ESC on the bus - The ESC has to be configured to have an address from 0x80 to 0x87. This parameter needs to match the configured value. - 128 - 135 - - 0x80 - 0x81 - 0x82 - 0x83 - 0x84 - 0x85 - 0x86 - 0x87 - - - - Number of encoder counts for one wheel revolution - The default value of 1200 corresponds to the default configuration of the Aion R1 rover. - 1 - - - - - Tuning parameter for corner cutting - The geometric ideal acceptance radius is multiplied by this factor to account for kinematic and dynamic effects. Higher value -> The rover starts to cut the corner earlier. - 1 - 100 - 2 - 0.01 - - - Maximum acceptance radius for the waypoints - The controller scales the acceptance radius based on the angle between the previous, current and next waypoint. Higher value -> smoother trajectory at the cost of how close the rover gets to the waypoint (Set to -1 to disable corner cutting). - -1 - 100 - m - 2 - 0.01 - - - Integral gain for lateral acceleration controller - 0 - 100 - 3 - 0.001 - - - Proportional gain for lateral acceleration controller - 0 - 100 - 2 - 0.01 - - - Maximum acceleration for the rover - This is used for the acceleration slew rate. - -1 - 100 - m/s^2 - 2 - 0.01 - - - Maximum deceleration for the rover - This is used for the deceleration slew rate, the feed-forward term for the speed controller during missions and the corner slow down effect. Note: For the corner slow down effect RA_MAX_JERK also has to be set. - -1 - 100 - m/s^2 - 2 - 0.01 - - - Maximum jerk - Limit for forwards acc/deceleration change. This is used for the corner slow down effect. Note: RA_MAX_DECEL also has to be set for this to be enabled. - -1 - 100 - m/s^3 - 2 - 0.01 - - - Maximum allowed lateral acceleration - This parameter is used to cap the lateral acceleration and map controller inputs to desired lateral acceleration in Acro, Stabilized and Position mode. - 0.01 - 1000 - m/s^2 - 2 - 0.01 - - - Maximum allowed speed - This is the maximum allowed speed setpoint when driving in a mode that uses closed loop speed control. - -1 - 100 - m/s - 2 - 0.01 - - - Maximum steering angle - The maximum angle that the rover can steer - 0.1 - 1.5708 - rad - 2 - 0.01 - - - Maximum steering rate for the rover - -1 - 1000 - deg/s - 2 - 0.01 - - - Speed the rover drives at maximum throttle - This parameter is used to calculate the feedforward term of the closed loop speed control which linearly maps desired speeds to normalized motor commands [-1. 1]. A good starting point is the observed ground speed when the rover drives at maximum throttle in manual mode. Increase this parameter if the rover is faster than the setpoint, and decrease if the rover is slower. - -1 - 100 - m/s - 2 - 0.01 - - - Integral gain for ground speed controller - 0 - 100 - 3 - 0.001 - - - Proportional gain for ground speed controller - 0 - 100 - 2 - 0.01 - - - Wheel base - Distance from the front to the rear axle - 0.001 - 100 + Return mode return altitude + Default minimum altitude above destination (e.g. home, safe point, landing pattern) for return flight. +The vehicle will climb to this altitude when Return mode is enganged, unless it currently is flying higher already. +This is affected by RTL_MIN_DIST and RTL_CONE_ANG. + 0 m - 3 - 0.001 + 1 + 0.5 + + + Return type + Return mode destination and flight path (home location, rally point, mission landing pattern, reverse mission) + + Return to closest safe point (home or rally point) via direct path. + Return to closest safe point other than home (mission landing pattern or rally point), via direct path. If no mission landing or rally points are defined return home via direct path. Always choose closest safe landing point if vehicle is a VTOL in hover mode. + Return to a planned mission landing, if available, using the mission path, else return to home via the reverse mission path. Do not consider rally points. + Return via direct path to closest destination: home, start of mission landing pattern or safe point. If the destination is a mission landing pattern, follow the pattern to land. + Return to the planned mission landing, or to home via the reverse mission path, whichever is closer by counting waypoints. Do not consider rally points. + Return directly to safe landing point (do not consider mission landing and Home) + - - - Maximum acceleration - Maximum acceleration is used to limit the acceleration of the rover - 0 - 100 - m/s^2 - 2 - 0.01 + + + RTL force approach landing + Only consider RTL point, if it has an approach defined. - - Maximum deceleration - Maximum decelaration is used to limit the deceleration of the rover. Set to -1 to disable, causing the rover to decelerate as fast as possible. Caution: This disables the slow down effect in auto modes. - -1 - 100 - m/s^2 - 2 - 0.01 + + RTL time estimate safety margin factor + Safety factor that is used to scale the actual RTL time estimate. +Time with margin = RTL_TIME_FACTOR * time + RTL_TIME_MARGIN + 1.0 + 2.0 + 1 + 0.1 - - Maximum jerk - Limit for forwards acc/deceleration change. + + RTL time estimate safety margin offset + Margin that is added to the time estimate, after it has already been scaled +Time with margin = RTL_TIME_FACTOR * time + RTL_TIME_MARGIN 0 - 100 - m/s^3 - 2 - 0.01 + 3600 + s + 1 + 1 - - Maximum speed setpoint - This parameter is used to cap desired forward speed and map controller inputs to desired speeds in Position mode. - 0 + + + + Tuning parameter for corner cutting + The geometric ideal acceptance radius is multiplied by this factor +to account for kinematic and dynamic effects. +Higher value -> The rover starts to cut the corner earlier. + 1 100 - m/s 2 0.01 - - Speed the rover drives at maximum throttle - This parameter is used to calculate the feedforward term of the closed loop speed control which linearly maps desired speeds to normalized motor commands [-1. 1]. A good starting point is the observed ground speed when the rover drives at maximum throttle in manual mode. Increase this parameter if the rover is faster than the setpoint, and decrease if the rover is slower. - 0 + + Maximum acceptance radius for the waypoints + The controller scales the acceptance radius based on the angle between +the previous, current and next waypoint. +Higher value -> smoother trajectory at the cost of how close the rover gets +to the waypoint (Set to -1 to disable corner cutting). + -1 100 - m/s + m 2 0.01 - - Yaw rate turning left/right wheels at max speed in opposite directions - This parameter is used to calculate the feedforward term of the closed loop yaw rate control. The controller first calculates the required speed difference between the left and right motor to achieve the desired yaw rate. This desired speed difference is then linearly mapped to normalized motor commands. A good starting point is twice the speed the rover drives at maximum throttle (RD_MAX_THR_SPD)). Increase this parameter if the rover turns faster than the setpoint, and decrease if the rover turns slower. + + Maximum steering angle 0 - 100 - m/s + 1.5708 + rad 2 0.01 - - Maximum allowed yaw acceleration for the rover - This parameter is used to cap desired yaw acceleration. This is used to adjust incoming yaw rate setpoints to a feasible yaw rate setpoint based on the physical limitation on how fast the yaw rate can change. This leads to a smooth setpoint trajectory for the closed loop yaw rate controller to track. Set to -1 to disable. + + Steering rate limit + Set to -1 to disable. -1 1000 - deg/s^2 - 2 - 0.01 - - - Maximum allowed yaw rate for the rover - This parameter is used to cap desired yaw rates and map controller inputs to desired yaw rates in Acro,Stabilized and Position mode. - 0.01 - 1000 deg/s 2 0.01 - - Tuning parameter for the speed reduction during waypoint transition - The waypoint transition speed is calculated as: Transition_speed = Maximum_speed * (1 - normalized_transition_angle * RM_MISS_VEL_GAIN) The normalized transition angle is the angle between the line segment from prev-curr WP and curr-next WP interpolated from [0, 180] -> [0, 1]. Higher value -> More speed reduction during waypoint transitions. - 0.05 - 100 - 2 - 0.01 - - - Integral gain for closed loop forward speed controller + + Wheel base + Distance from the front to the rear axle. 0 100 + m 3 - 0.01 + 0.001 - - Proportional gain for closed loop forward speed controller + + + + Proportional gain for closed loop yaw controller 0 100 3 0.01 + + Yaw error threshhold to switch from driving to spot turning - This threshold is used for the state machine to switch from driving to turning based on the error between the desired and actual yaw. It is also used as the threshold whether the rover should come to a smooth stop at the next waypoint. This slow down effect is active if the angle between the line segments from prevWP-currWP and currWP-nextWP is smaller then 180 - RD_TRANS_DRV_TRN. + This threshold is used for the state machine to switch from driving to turning based on the +error between the desired and actual yaw. It is also used as the threshold whether the rover should come +to a smooth stop at the next waypoint. This slow down effect is active if the angle between the +line segments from prevWP-currWP and currWP-nextWP is smaller then 180 - RD_TRANS_DRV_TRN. 0.001 3.14159 rad @@ -20037,367 +12151,264 @@ Yaw error threshhold to switch from spot turning to driving + This threshold is used for the state machine to switch from turning to driving based on the +error between the desired and actual yaw. 0.001 3.14159 rad 3 0.01 - + Wheel track - Distance from the center of the right wheel to the center of the left wheel - 0.001 + Distance from the center of the right wheel to the center of the left wheel. + 0 100 m 3 0.001 - - Integral gain for closed loop yaw controller - 0 - 100 + + Yaw stick gain for Manual mode + Assign value <1.0 to decrease stick response for yaw control. + 0.1 + 1 3 0.01 - - Proportional gain for closed loop yaw controller - 0 - 100 - 3 + + + + Threshold to update course control in manual position mode + Threshold for the angle between the active cruise direction and the cruise direction given +by the stick inputs. +This can be understood as a deadzone for the combined stick inputs for forward/backwards +and lateral speed which defines a course direction. + 0 + 3.14 + rad + 2 0.01 - - Integral gain for closed loop yaw rate controller + + Wheel track + Distance from the center of the right wheel to the center of the left wheel. 0 100 + m 3 - 0.01 + 0.001 - - Proportional gain for closed loop yaw rate controller - 0 - 100 + + Yaw stick gain for Manual mode + Assign value <1.0 to decrease stick response for yaw control. + 0.1 + 1 3 0.01 - - - Maximum acceleration - Maximum acceleration is used to limit the acceleration of the rover - 0 - 100 - m/s^2 + + + Yaw acceleration limit + Used to cap how quickly the magnitude of yaw rate setpoints can increase. +Set to -1 to disable. + -1 + 10000 + deg/s^2 2 0.01 - - Maximum deceleration - Maximum decelaration is used to limit the deceleration of the rover. Set to -1 to disable, causing the rover to decelerate as fast as possible. Caution: Disabling the deceleration limit also disables the slow down effect in auto modes. + + Yaw deceleration limit + Used to cap how quickly the magnitude of yaw rate setpoints can decrease. +Set to -1 to disable. -1 - 100 - m/s^2 + 10000 + deg/s^2 2 0.01 - - Maximum jerk - Limit for forwards acc/deceleration change. + + Yaw rate expo factor + Exponential factor for tuning the input curve shape. +0 Purely linear input curve +1 Purely cubic input curve 0 - 100 - m/s^3 + 1 2 - 0.01 - - Maximum speed the rover is allowed to drive - This parameter is used cap the maximum speed the rover is allowed to drive and to map stick inputs to desired speeds in position mode. - 0 - 100 - m/s + + Yaw rate correction factor + Multiplicative correction factor for the feedforward mapping of the yaw rate controller. +Increase this value (x > 1) if the measured yaw rate is lower than the setpoint, decrease (0 < x < 1) otherwise. +Note: Tuning this is particularly useful for skid-steered rovers, or rovers with misaligned wheels/steering axes +that cause a lot of friction when turning. + 0.01 + 10000 2 0.01 - - Speed the rover drives at maximum throttle - This parameter is used to calculate the feedforward term of the closed loop speed control which linearly maps desired speeds to normalized motor commands [-1. 1]. A good starting point is the observed ground speed when the rover drives at maximum throttle in manual mode. Increase this parameter if the rover is faster than the setpoint, and decrease if the rover is slower. + + Integral gain for closed loop yaw rate controller 0 100 - m/s - 2 + 3 0.01 - - Yaw rate turning left/right wheels at max speed in opposite directions - This parameter is used to calculate the feedforward term of the closed loop yaw rate control. The controller first calculates the required speed difference between the left and right motor to achieve the desired yaw rate. This desired speed difference is then linearly mapped to normalized motor commands. A good starting point is twice the speed the rover drives at maximum throttle (RM_MAX_THRTL_SPD)). Increase this parameter if the rover turns faster than the setpoint, and decrease if the rover turns slower. + + Yaw rate limit + Used to cap yaw rate setpoints and map controller inputs to yaw rate setpoints +in Acro, Stabilized and Position mode. 0 - 100 - m/s - 2 - 0.01 - - - Maximum allowed yaw acceleration for the rover - This parameter is used to cap desired yaw acceleration. This is used to adjust incoming yaw rate setpoints to a feasible yaw rate setpoint based on the physical limitation on how fast the yaw rate can change. This leads to a smooth setpoint trajectory for the closed loop yaw rate controller to track. Set to -1 to disable. - -1 - 1000 - deg/s^2 - 2 - 0.01 - - - Maximum allowed yaw rate for the rover - This parameter is used to cap desired yaw rates and map controller inputs to desired yaw rates in acro mode. - 0.01 - 1000 + 10000 deg/s 2 0.01 - - Tuning parameter for the velocity reduction during waypoint transition - The waypoint transition speed is calculated as: Transition_speed = Maximum_speed * (1 - normalized_transition_angle * RM_MISS_VEL_GAIN) The normalized transition angle is the angle between the line segment from prev-curr WP and curr-next WP interpolated from [0, 180] -> [0, 1]. Higher value -> More velocity reduction during cornering. - 0.05 - 100 - 2 - 0.01 - - - Integral gain for ground speed controller + + Proportional gain for closed loop yaw rate controller 0 100 3 0.01 - - Proportional gain for speed controller + + Yaw rate measurement threshold + The minimum threshold for the yaw rate measurement not to be interpreted as zero. 0 100 - 3 + deg/s + 2 0.01 - - Wheel track - Distance from the center of the right wheels to the center of the left wheels. - 0.001 - 100 - m - 3 - 0.001 - - - Integral gain for closed loop yaw controller + + Yaw stick deadzone + Percentage of stick input range that will be interpreted as zero around the stick centered value. 0 - 100 + 1 2 0.01 - - Proportional gain for closed loop yaw controller + + Yaw rate super expo factor + "Superexponential" factor for refining the input curve shape tuned using RO_YAW_EXPO. +0 Pure Expo function +0.7 reasonable shape enhancement for intuitive stick feel +0.95 very strong bent input curve only near maxima have effect 0 - 100 + 0.95 2 - 0.01 - - Integral gain for the closed-loop yaw rate controller - 0 + + + + Acceleration limit + Set to -1 to disable. +For mecanum rovers this limit is used for longitudinal and lateral acceleration. + -1 100 + m/s^2 2 0.01 - - Proportional gain for the closed-loop yaw rate controller - 0 + + Deceleration limit + Set to -1 to disable. +Note that if it is disabled the rover will not slow down when approaching waypoints in auto modes. +For mecanum rovers this limit is used for longitudinal and lateral deceleration. + -1 100 + m/s^2 2 0.01 - - - - L1 damping - Damping factor for L1 control. - 0.6 - 0.9 + + Jerk limit + Set to -1 to disable. +Note that if it is disabled the rover will not slow down when approaching waypoints in auto modes. +For mecanum rovers this limit is used for longitudinal and lateral jerk. + -1 + 100 + m/s^3 2 - 0.05 - - - L1 distance - This is the distance at which the next waypoint is activated. This should be set to about 2-4x of GND_WHEEL_BASE and not smaller than one meter (due to GPS accuracy). - 1.0 - 50.0 - m - 1 - 0.1 - - - L1 period - This is the L1 distance and defines the tracking point ahead of the rover it's following. Use values around 2-5m for a 0.3m wheel base. Tuning instructions: Shorten slowly during tuning until response is sharp without oscillation. - 0.5 - 50.0 - m - 1 - 0.5 - - - Max manual yaw rate - 0.0 - 400 - deg/s - 1 - - - Maximum turn angle for Ackerman steering - At a control output of 0, the steering wheels are at 0 radians. At a control output of 1, the steering wheels are at GND_MAX_ANG radians. - 0.0 - 3.14159 - rad - 3 0.01 - - Speed proportional gain - This is the derivative gain for the speed closed loop controller - 0.00 - 50.0 - %m/s - 3 - 0.005 - - - Speed Integral gain - This is the integral gain for the speed closed loop controller - 0.00 - 50.0 - %m/s - 3 - 0.005 - - - Speed integral maximum value - This is the maxim value the integral can reach to prevent wind-up. - 0.005 - 50.0 - %m/s - 3 - 0.005 - - - Maximum ground speed - 0.0 - 40 + + Speed the rover drives at maximum throttle + Used to linearly map speeds [m/s] to throttle values [-1. 1]. + 0 + 100 m/s - 1 - 0.5 - - - Speed proportional gain - This is the proportional gain for the speed closed loop controller - 0.005 - 50.0 - %m/s - 3 - 0.005 + 2 + 0.01 - - Speed to throttle scaler - This is a gain to map the speed control output to the throttle linearly. - 0.005 - 50.0 - %m/s + + Integral gain for ground speed controller + 0 + 100 3 - 0.005 + 0.001 - - Trim ground speed - 0.0 - 40 + + Speed limit + Used to cap speed setpoints and map controller inputs to speed setpoints in Position mode. + -1 + 100 m/s - 1 - 0.5 - - - Control mode for speed - This allows the user to choose between closed loop gps speed or open loop cruise throttle speed - 0 - 1 - - open loop control - close the loop with gps speed - - - - Cruise throttle - This is the throttle setting required to achieve the desired cruise speed. 10% is ok for a traxxas stampede vxl with ESC set to training mode - 0.0 - 1.0 - norm 2 0.01 - - Throttle limit max - This is the maximum throttle % that can be used by the controller. For a Traxxas stampede vxl with the ESC set to training, 30 % is enough - 0.0 - 1.0 - norm + + Proportional gain for ground speed controller + 0 + 100 2 0.01 - - Throttle limit min - This is the minimum throttle % that can be used by the controller. Set to 0 for rover - 0.0 - 1.0 - norm + + Tuning parameter for the speed reduction based on the course error + Reduced_speed = RO_MAX_THR_SPEED * (1 - normalized_course_error * RO_SPEED_RED) +The normalized course error is the angle between the current course and the bearing setpoint +interpolated from [0, 180] -> [0, 1]. +Higher value -> More speed reduction. +Note: This is also used to calculate the speed at which the vehicle arrives at a waypoint in auto modes. +Set to -1 to disable bearing error based speed reduction. + -1 + 100 2 0.01 - - Distance from front axle to rear axle - A value of 0.31 is typical for 1/10 RC cars. - 0.0 - m - 3 + + Speed measurement threshold + Set to -1 to disable. +The minimum threshold for the speed measurement not to be interpreted as zero. + 0 + 100 + m/s + 2 0.01 - - Specifies which heading should be held during the runway takeoff ground roll - 0: airframe heading when takeoff is initiated 1: position control along runway direction (bearing defined from vehicle position on takeoff initiation to MAV_CMD_TAKEOFF position defined by operator) - 0 - 1 - - Airframe - Runway - - - Max throttle during runway takeoff + Throttle during runway takeoff 0.0 1.0 norm 2 0.01 - - NPFG period while steering on runway - 1.0 - 100.0 - s - 1 - 0.1 - Enable use of yaw stick for nudging the wheel during runway ground roll - This is useful when map, GNSS, or yaw errors on ground are misaligned with what the operator intends for takeoff course. Particularly useful for skinny runways or if the wheel servo is a bit off trim. + This is useful when map, GNSS, or yaw errors on ground are misaligned with what the operator intends for takeoff course. +Particularly useful for skinny runways or if the wheel servo is a bit off trim. Pitch setpoint during taxi / before takeoff rotation airspeed is reached - A taildragger with steerable wheel might need to pitch up a little to keep its wheel on the ground before airspeed to takeoff is reached. + A taildragger with steerable wheel might need to pitch up +a little to keep its wheel on the ground before airspeed +to takeoff is reached. -10.0 20.0 deg @@ -20414,7 +12425,9 @@ Takeoff rotation airspeed - The calibrated airspeed threshold during the takeoff ground roll when the plane should start rotating (pitching up). Must be less than the takeoff airspeed, will otherwise be capped at the takeoff airpeed (see FW_TKO_AIRSPD). If set <= 0.0, defaults to 0.9 * takeoff airspeed (see FW_TKO_AIRSPD) + The calibrated airspeed threshold during the takeoff ground roll when the plane should start rotating (pitching up). +Must be less than the takeoff airspeed, will otherwise be capped at the takeoff airpeed (see FW_TKO_AIRSPD). +If set <= 0.0, defaults to 0.9 * takeoff airspeed (see FW_TKO_AIRSPD) -1.0 m/s 1 @@ -20433,13 +12446,16 @@ - - Logfile Encryption algorithm - Selects the algorithm used for logfile encryption - - Disabled - XChaCha20 - + + Logging Backend (integer bitmask) + If no logging is set the logger will not be started. Set bits true to enable: 0: SD card logging 1: Mavlink logging + 0 + 3 + True + + SD card logging + Mavlink logging + Battery-only Logging @@ -20450,24 +12466,12 @@ If there are more log directories than this value, the system will delete the oldest directories during startup. In addition, the system will delete old logs if there is not enough free space left. The minimum amount is 300 MB. If this is set to 0, old directories will only be removed if the free space falls below the minimum. Note: this does not apply to mission log files. 0 1000 - true - - - Logfile Encryption key exchange key - If the logfile is encrypted using a symmetric key algorithm, the used encryption key is generated at logging start and stored on the sdcard RSA2048 encrypted using this key. - 0 - 255 - - - Logfile Encryption key index - Selects the key in keystore, used for encrypting the log. When using a symmetric encryption algorithm, the key is generated at logging start and kept stored in this index. For symmetric algorithms, the key is volatile and valid only for the duration of logging. The key is stored in encrypted format on the sdcard alongside the logfile, using an RSA2048 key defined by the SDLOG_EXCHANGE_KEY - 0 - 255 + True Mission Log If enabled, a small additional "mission" log file will be written to the SD card. The log contains just those messages that are useful for tasks like generating flight statistics and geotagging. The different modes can be used to further reduce the logged data (and thus the log file size). For example, choose geotagging mode to only log data required for geotagging. Note that the normal/full log is still created, and contains all the data in the mission log (and more). - true + True Disabled All mission messages @@ -20476,10 +12480,9 @@ Logging Mode - Determines when to start and stop logging. By default, logging is started when arming the system, and stopped when disarming. - true + Determines when to start and stop logging. By default, logging is started when arming the system, and stopped when disarming. Note: The logging start/end points that can be configured here only apply to SD logging. The mavlink backend is started/stopped independently of these points. + True - disabled when armed until disarm (default) from boot until disarm from boot until shutdown @@ -20489,10 +12492,10 @@ Logging topic profile (integer bitmask) - This integer bitmask controls the set and rates of logged topics. The default allows for general log analysis while keeping the log file size reasonably small. Enabling multiple sets leads to higher bandwidth requirements and larger log files. Set bits true to enable: 0 : Default set (used for general log analysis) 1 : Full rate estimator (EKF2) replay topics 2 : Topics for thermal calibration (high rate raw IMU and Baro sensor data) 3 : Topics for system identification (high rate actuator control and IMU data) 4 : Full rates for analysis of fast maneuvers (RC, attitude, rates and actuators) 5 : Debugging topics (debug_*.msg topics, for custom code) 6 : Topics for sensor comparison (low rate raw IMU, Baro and magnetometer data) 7 : Topics for computer vision and collision avoidance 8 : Raw FIFO high-rate IMU (Gyro) 9 : Raw FIFO high-rate IMU (Accel) 10: Logging of mavlink tunnel message (useful for payload communication debugging) + This integer bitmask controls the set and rates of logged topics. The default allows for general log analysis while keeping the log file size reasonably small. Enabling multiple sets leads to higher bandwidth requirements and larger log files. Set bits true to enable: 0 : Default set (used for general log analysis) 1 : Full rate estimator (EKF2) replay topics 2 : Topics for thermal calibration (high rate raw IMU and Baro sensor data) 3 : Topics for system identification (high rate actuator control and IMU data) 4 : Full rates for analysis of fast maneuvers (RC, attitude, rates and actuators) 5 : Debugging topics (debug_*.msg topics, for custom code) 6 : Topics for sensor comparison (low rate raw IMU, Baro and magnetometer data) 7 : Topics for computer vision and collision prevention 8 : Raw FIFO high-rate IMU (Gyro) 9 : Raw FIFO high-rate IMU (Accel) 10: Logging of mavlink tunnel message (useful for payload communication debugging) 0 - 2047 - true + 4095 + True Default set (general log analysis) Estimator replay (EKF2) @@ -20505,6 +12508,7 @@ Raw FIFO high-rate IMU (Gyro) Raw FIFO high-rate IMU (Accel) Mavlink tunnel message logging + High rate sensors @@ -20518,238 +12522,39 @@ Log UUID If set to 1, add an ID to the log, which uniquely identifies the vehicle - - - - Simulator Battery drain interval - 1 - 86400 - s - 1 - - - Simulator Battery enabled - Enable or disable the internal battery simulation. This is useful when the battery is simulated externally and interfaced with PX4 through MAVLink for example. - - - Simulator Battery minimal percentage - Can be used to alter the battery level during SITL- or HITL-simulation on the fly. Particularly useful for testing different low-battery behaviour. - 0 - 100 - % - 0.1 - - - - - Accelerometer 0 calibration device ID - Device ID of the accelerometer this calibration applies to. - 3 - - - Accelerometer 0 priority - 3 - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Accelerometer 0 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. - -1 - 40 - - Internal - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - Roll 180° - Roll 180°, Yaw 45° - Roll 180°, Yaw 90° - Roll 180°, Yaw 135° - Pitch 180° - Roll 180°, Yaw 225° - Roll 180°, Yaw 270° - Roll 180°, Yaw 315° - Roll 90° - Roll 90°, Yaw 45° - Roll 90°, Yaw 90° - Roll 90°, Yaw 135° - Roll 270° - Roll 270°, Yaw 45° - Roll 270°, Yaw 90° - Roll 270°, Yaw 135° - Pitch 90° - Pitch 270° - Pitch 180°, Yaw 90° - Pitch 180°, Yaw 270° - Roll 90°, Pitch 90° - Roll 180°, Pitch 90° - Roll 270°, Pitch 90° - Roll 90°, Pitch 180° - Roll 270°, Pitch 180° - Roll 90°, Pitch 270° - Roll 180°, Pitch 270° - Roll 270°, Pitch 270° - Roll 90°, Pitch 180°, Yaw 90° - Roll 90°, Yaw 270° - Roll 90°, Pitch 68°, Yaw 293° - Pitch 315° - Roll 90°, Pitch 315° - - - - Accelerometer 0 X-axis offset - m/s^2 - 3 - - - Accelerometer 0 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 0 Y-axis offset - m/s^2 - 3 - - - Accelerometer 0 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 0 Z-axis offset - m/s^2 - 3 - - - Accelerometer 0 Z-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 1 calibration device ID - Device ID of the accelerometer this calibration applies to. - 3 - - - Accelerometer 1 priority - 3 - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Accelerometer 1 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. - -1 - 40 - - Internal - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - Roll 180° - Roll 180°, Yaw 45° - Roll 180°, Yaw 90° - Roll 180°, Yaw 135° - Pitch 180° - Roll 180°, Yaw 225° - Roll 180°, Yaw 270° - Roll 180°, Yaw 315° - Roll 90° - Roll 90°, Yaw 45° - Roll 90°, Yaw 90° - Roll 90°, Yaw 135° - Roll 270° - Roll 270°, Yaw 45° - Roll 270°, Yaw 90° - Roll 270°, Yaw 135° - Pitch 90° - Pitch 270° - Pitch 180°, Yaw 90° - Pitch 180°, Yaw 270° - Roll 90°, Pitch 90° - Roll 180°, Pitch 90° - Roll 270°, Pitch 90° - Roll 90°, Pitch 180° - Roll 270°, Pitch 180° - Roll 90°, Pitch 270° - Roll 180°, Pitch 270° - Roll 270°, Pitch 270° - Roll 90°, Pitch 180°, Yaw 90° - Roll 90°, Yaw 270° - Roll 90°, Pitch 68°, Yaw 293° - Pitch 315° - Roll 90°, Pitch 315° - - - - Accelerometer 1 X-axis offset - m/s^2 - 3 - - - Accelerometer 1 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 1 Y-axis offset - m/s^2 - 3 - - - Accelerometer 1 Y-axis scaling factor - 0.1 - 3.0 - 3 + + + + Simulator Battery drain interval + 1 + 86400 + s + 1 - - Accelerometer 1 Z-axis offset - m/s^2 - 3 + + Simulator Battery enabled + Enable or disable the internal battery simulation. This is useful +when the battery is simulated externally and interfaced with PX4 +through MAVLink for example. - - Accelerometer 1 Z-axis scaling factor - 0.1 - 3.0 - 3 + + Simulator Battery minimal percentage + Can be used to alter the battery level during SITL- or HITL-simulation on the fly. +Particularly useful for testing different low-battery behaviour. + 0 + 100 + % + 0.1 - - Accelerometer 2 calibration device ID + + + + Accelerometer 0 calibration device ID Device ID of the accelerometer this calibration applies to. 3 - - Accelerometer 2 priority + + Accelerometer 0 priority 3 Uninitialized @@ -20761,8 +12566,8 @@ Max - - Accelerometer 2 rotation relative to airframe + + Accelerometer 0 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -20811,46 +12616,46 @@ Roll 90°, Pitch 315° - - Accelerometer 2 X-axis offset + + Accelerometer 0 X-axis offset m/s^2 3 - - Accelerometer 2 X-axis scaling factor + + Accelerometer 0 X-axis scaling factor 0.1 3.0 3 - - Accelerometer 2 Y-axis offset + + Accelerometer 0 Y-axis offset m/s^2 3 - - Accelerometer 2 Y-axis scaling factor + + Accelerometer 0 Y-axis scaling factor 0.1 3.0 3 - - Accelerometer 2 Z-axis offset + + Accelerometer 0 Z-axis offset m/s^2 3 - - Accelerometer 2 Z-axis scaling factor + + Accelerometer 0 Z-axis scaling factor 0.1 3.0 3 - - Accelerometer 3 calibration device ID + + Accelerometer 1 calibration device ID Device ID of the accelerometer this calibration applies to. 3 - - Accelerometer 3 priority + + Accelerometer 1 priority 3 Uninitialized @@ -20862,8 +12667,8 @@ Max - - Accelerometer 3 rotation relative to airframe + + Accelerometer 1 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -20912,206 +12717,47 @@ Roll 90°, Pitch 315° - - Accelerometer 3 X-axis offset - m/s^2 - 3 - - - Accelerometer 3 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 3 Y-axis offset + + Accelerometer 1 X-axis offset m/s^2 3 - - Accelerometer 3 Y-axis scaling factor + + Accelerometer 1 X-axis scaling factor 0.1 3.0 3 - - Accelerometer 3 Z-axis offset + + Accelerometer 1 Y-axis offset m/s^2 - 3 - - - Accelerometer 3 Z-axis scaling factor - 0.1 - 3.0 - 3 - - - Barometer 0 calibration device ID - Device ID of the barometer this calibration applies to. - - - Barometer 0 offset - 3 - - - Barometer 0 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Barometer 1 calibration device ID - Device ID of the barometer this calibration applies to. - - - Barometer 1 offset - 3 - - - Barometer 1 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Barometer 2 calibration device ID - Device ID of the barometer this calibration applies to. - - - Barometer 2 offset - 3 - - - Barometer 2 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Barometer 3 calibration device ID - Device ID of the barometer this calibration applies to. - - - Barometer 3 offset - 3 - - - Barometer 3 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Gyroscope 0 calibration device ID - Device ID of the gyroscope this calibration applies to. - - - Gyroscope 0 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Gyroscope 0 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. - -1 - 40 - - Internal - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - Roll 180° - Roll 180°, Yaw 45° - Roll 180°, Yaw 90° - Roll 180°, Yaw 135° - Pitch 180° - Roll 180°, Yaw 225° - Roll 180°, Yaw 270° - Roll 180°, Yaw 315° - Roll 90° - Roll 90°, Yaw 45° - Roll 90°, Yaw 90° - Roll 90°, Yaw 135° - Roll 270° - Roll 270°, Yaw 45° - Roll 270°, Yaw 90° - Roll 270°, Yaw 135° - Pitch 90° - Pitch 270° - Pitch 180°, Yaw 90° - Pitch 180°, Yaw 270° - Roll 90°, Pitch 90° - Roll 180°, Pitch 90° - Roll 270°, Pitch 90° - Roll 90°, Pitch 180° - Roll 270°, Pitch 180° - Roll 90°, Pitch 270° - Roll 180°, Pitch 270° - Roll 270°, Pitch 270° - Roll 90°, Pitch 180°, Yaw 90° - Roll 90°, Yaw 270° - Roll 90°, Pitch 68°, Yaw 293° - Pitch 315° - Roll 90°, Pitch 315° - + 3 - - Gyroscope 0 X-axis offset - rad/s + + Accelerometer 1 Y-axis scaling factor + 0.1 + 3.0 3 - - Gyroscope 0 Y-axis offset - rad/s + + Accelerometer 1 Z-axis offset + m/s^2 3 - - Gyroscope 0 Z-axis offset - rad/s + + Accelerometer 1 Z-axis scaling factor + 0.1 + 3.0 3 - - Gyroscope 1 calibration device ID - Device ID of the gyroscope this calibration applies to. + + Accelerometer 2 calibration device ID + Device ID of the accelerometer this calibration applies to. + 3 - - Gyroscope 1 priority + + Accelerometer 2 priority + 3 Uninitialized Disabled @@ -21122,8 +12768,8 @@ Max - - Gyroscope 1 rotation relative to airframe + + Accelerometer 2 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -21172,27 +12818,47 @@ Roll 90°, Pitch 315° - - Gyroscope 1 X-axis offset - rad/s + + Accelerometer 2 X-axis offset + m/s^2 3 - - Gyroscope 1 Y-axis offset - rad/s + + Accelerometer 2 X-axis scaling factor + 0.1 + 3.0 3 - - Gyroscope 1 Z-axis offset - rad/s + + Accelerometer 2 Y-axis offset + m/s^2 3 - - Gyroscope 2 calibration device ID - Device ID of the gyroscope this calibration applies to. + + Accelerometer 2 Y-axis scaling factor + 0.1 + 3.0 + 3 - - Gyroscope 2 priority + + Accelerometer 2 Z-axis offset + m/s^2 + 3 + + + Accelerometer 2 Z-axis scaling factor + 0.1 + 3.0 + 3 + + + Accelerometer 3 calibration device ID + Device ID of the accelerometer this calibration applies to. + 3 + + + Accelerometer 3 priority + 3 Uninitialized Disabled @@ -21203,8 +12869,8 @@ Max - - Gyroscope 2 rotation relative to airframe + + Accelerometer 3 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -21253,27 +12919,125 @@ Roll 90°, Pitch 315° - - Gyroscope 2 X-axis offset - rad/s + + Accelerometer 3 X-axis offset + m/s^2 3 - - Gyroscope 2 Y-axis offset - rad/s + + Accelerometer 3 X-axis scaling factor + 0.1 + 3.0 3 - - Gyroscope 2 Z-axis offset - rad/s + + Accelerometer 3 Y-axis offset + m/s^2 3 - - Gyroscope 3 calibration device ID + + Accelerometer 3 Y-axis scaling factor + 0.1 + 3.0 + 3 + + + Accelerometer 3 Z-axis offset + m/s^2 + 3 + + + Accelerometer 3 Z-axis scaling factor + 0.1 + 3.0 + 3 + + + Barometer 0 calibration device ID + Device ID of the barometer this calibration applies to. + + + Barometer 0 offset + 3 + + + Barometer 0 priority + + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max + + + + Barometer 1 calibration device ID + Device ID of the barometer this calibration applies to. + + + Barometer 1 offset + 3 + + + Barometer 1 priority + + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max + + + + Barometer 2 calibration device ID + Device ID of the barometer this calibration applies to. + + + Barometer 2 offset + 3 + + + Barometer 2 priority + + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max + + + + Barometer 3 calibration device ID + Device ID of the barometer this calibration applies to. + + + Barometer 3 offset + 3 + + + Barometer 3 priority + + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max + + + + Gyroscope 0 calibration device ID Device ID of the gyroscope this calibration applies to. - - Gyroscope 3 priority + + Gyroscope 0 priority Uninitialized Disabled @@ -21284,8 +13048,8 @@ Max - - Gyroscope 3 rotation relative to airframe + + Gyroscope 0 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -21334,34 +13098,27 @@ Roll 90°, Pitch 315° - - Gyroscope 3 X-axis offset + + Gyroscope 0 X-axis offset rad/s 3 - - Gyroscope 3 Y-axis offset + + Gyroscope 0 Y-axis offset rad/s 3 - - Gyroscope 3 Z-axis offset + + Gyroscope 0 Z-axis offset rad/s 3 - - Magnetometer 0 calibration device ID - Device ID of the magnetometer this calibration applies to. - - - Magnetometer 0 Custom Euler Pitch Angle - Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" - -180 - 180 - deg + + Gyroscope 1 calibration device ID + Device ID of the gyroscope this calibration applies to. - - Magnetometer 0 priority + + Gyroscope 1 priority Uninitialized Disabled @@ -21371,19 +13128,12 @@ High Max - - - Magnetometer 0 Custom Euler Roll Angle - Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 0 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. Set to "Custom Euler Angle" to define the rotation using CAL_MAG0_ROLL, CAL_MAG0_PITCH and CAL_MAG0_YAW. + + + Gyroscope 1 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 - 100 + 40 Internal No rotation @@ -21427,88 +13177,29 @@ Roll 90°, Pitch 68°, Yaw 293° Pitch 315° Roll 90°, Pitch 315° - Custom Euler Angle - - Magnetometer 0 X Axis throttle compensation - Coefficient describing linear relationship between X component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 0 X-axis off diagonal scale factor - 3 - - - Magnetometer 0 X-axis offset - gauss - 3 - - - Magnetometer 0 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 0 Custom Euler Yaw Angle - Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 0 Y Axis throttle compensation - Coefficient describing linear relationship between Y component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 0 Y-axis off diagonal scale factor - 3 - - - Magnetometer 0 Y-axis offset - gauss - 3 - - - Magnetometer 0 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 0 Z Axis throttle compensation - Coefficient describing linear relationship between Z component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] + + Gyroscope 1 X-axis offset + rad/s 3 - - Magnetometer 0 Z-axis off diagonal scale factor - - - Magnetometer 0 Z-axis offset - gauss + + Gyroscope 1 Y-axis offset + rad/s 3 - - Magnetometer 0 Z-axis scaling factor - 0.1 - 3.0 + + Gyroscope 1 Z-axis offset + rad/s 3 - - Magnetometer 1 calibration device ID - Device ID of the magnetometer this calibration applies to. - - - Magnetometer 1 Custom Euler Pitch Angle - Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" - -180 - 180 - deg + + Gyroscope 2 calibration device ID + Device ID of the gyroscope this calibration applies to. - - Magnetometer 1 priority + + Gyroscope 2 priority Uninitialized Disabled @@ -21519,18 +13210,11 @@ Max - - Magnetometer 1 Custom Euler Roll Angle - Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 1 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. Set to "Custom Euler Angle" to define the rotation using CAL_MAG1_ROLL, CAL_MAG1_PITCH and CAL_MAG1_YAW. + + Gyroscope 2 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 - 100 + 40 Internal No rotation @@ -21574,88 +13258,29 @@ Roll 90°, Pitch 68°, Yaw 293° Pitch 315° Roll 90°, Pitch 315° - Custom Euler Angle - - Magnetometer 1 X Axis throttle compensation - Coefficient describing linear relationship between X component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 1 X-axis off diagonal scale factor - 3 - - - Magnetometer 1 X-axis offset - gauss - 3 - - - Magnetometer 1 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 1 Custom Euler Yaw Angle - Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 1 Y Axis throttle compensation - Coefficient describing linear relationship between Y component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 1 Y-axis off diagonal scale factor - 3 - - - Magnetometer 1 Y-axis offset - gauss - 3 - - - Magnetometer 1 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 1 Z Axis throttle compensation - Coefficient describing linear relationship between Z component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] + + Gyroscope 2 X-axis offset + rad/s 3 - - Magnetometer 1 Z-axis off diagonal scale factor - - - Magnetometer 1 Z-axis offset - gauss + + Gyroscope 2 Y-axis offset + rad/s 3 - - Magnetometer 1 Z-axis scaling factor - 0.1 - 3.0 + + Gyroscope 2 Z-axis offset + rad/s 3 - - Magnetometer 2 calibration device ID - Device ID of the magnetometer this calibration applies to. - - - Magnetometer 2 Custom Euler Pitch Angle - Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" - -180 - 180 - deg + + Gyroscope 3 calibration device ID + Device ID of the gyroscope this calibration applies to. - - Magnetometer 2 priority + + Gyroscope 3 priority Uninitialized Disabled @@ -21666,18 +13291,11 @@ Max - - Magnetometer 2 Custom Euler Roll Angle - Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 2 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. Set to "Custom Euler Angle" to define the rotation using CAL_MAG2_ROLL, CAL_MAG2_PITCH and CAL_MAG2_YAW. + + Gyroscope 3 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 - 100 + 40 Internal No rotation @@ -21721,88 +13339,36 @@ Roll 90°, Pitch 68°, Yaw 293° Pitch 315° Roll 90°, Pitch 315° - Custom Euler Angle - - Magnetometer 2 X Axis throttle compensation - Coefficient describing linear relationship between X component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 2 X-axis off diagonal scale factor - 3 - - - Magnetometer 2 X-axis offset - gauss - 3 - - - Magnetometer 2 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 2 Custom Euler Yaw Angle - Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 2 Y Axis throttle compensation - Coefficient describing linear relationship between Y component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 2 Y-axis off diagonal scale factor - 3 - - - Magnetometer 2 Y-axis offset - gauss - 3 - - - Magnetometer 2 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 2 Z Axis throttle compensation - Coefficient describing linear relationship between Z component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 2 Z-axis off diagonal scale factor + + Gyroscope 3 X-axis offset + rad/s + 3 - - Magnetometer 2 Z-axis offset - gauss + + Gyroscope 3 Y-axis offset + rad/s 3 - - Magnetometer 2 Z-axis scaling factor - 0.1 - 3.0 + + Gyroscope 3 Z-axis offset + rad/s 3 - - Magnetometer 3 calibration device ID + + Magnetometer 0 calibration device ID Device ID of the magnetometer this calibration applies to. - - Magnetometer 3 Custom Euler Pitch Angle - Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + + Magnetometer 0 Custom Euler Pitch Angle + Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" -180 180 deg - - Magnetometer 3 priority + + Magnetometer 0 priority Uninitialized Disabled @@ -21813,16 +13379,17 @@ Max - - Magnetometer 3 Custom Euler Roll Angle - Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + + Magnetometer 0 Custom Euler Roll Angle + Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" -180 180 deg - - Magnetometer 3 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. Set to "Custom Euler Angle" to define the rotation using CAL_MAG3_ROLL, CAL_MAG3_PITCH and CAL_MAG3_YAW. + + Magnetometer 0 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. +Set to "Custom Euler Angle" to define the rotation using CAL_MAG0_ROLL, CAL_MAG0_PITCH and CAL_MAG0_YAW. -1 100 @@ -21871,498 +13438,282 @@ Custom Euler Angle - - Magnetometer 3 X Axis throttle compensation - Coefficient describing linear relationship between X component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] + + Magnetometer 0 X Axis throttle compensation + Coefficient describing linear relationship between +X component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] 3 - - Magnetometer 3 X-axis off diagonal scale factor + + Magnetometer 0 X-axis off diagonal scale factor 3 - - Magnetometer 3 X-axis offset + + Magnetometer 0 X-axis offset gauss 3 - - Magnetometer 3 X-axis scaling factor + + Magnetometer 0 X-axis scaling factor 0.1 3.0 3 - - Magnetometer 3 Custom Euler Yaw Angle - Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + + Magnetometer 0 Custom Euler Yaw Angle + Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" -180 180 deg - - Magnetometer 3 Y Axis throttle compensation - Coefficient describing linear relationship between Y component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 3 Y-axis off diagonal scale factor - 3 - - - Magnetometer 3 Y-axis offset - gauss - 3 - - - Magnetometer 3 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 3 Z Axis throttle compensation - Coefficient describing linear relationship between Z component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 3 Z-axis off diagonal scale factor - - - Magnetometer 3 Z-axis offset - gauss + + Magnetometer 0 Y Axis throttle compensation + Coefficient describing linear relationship between +Y component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] 3 - - Magnetometer 3 Z-axis scaling factor - 0.1 - 3.0 + + Magnetometer 0 Y-axis off diagonal scale factor 3 - - Type of magnetometer compensation - - Disabled - Throttle-based compensation - Current-based compensation (battery_status instance 0) - Current-based compensation (battery_status instance 1) - - - - Differential pressure sensor analog scaling - Pick the appropriate scaling from the datasheet. this number defines the (linear) conversion from voltage to Pascal (pa). For the MPXV7002DP this is 1000. NOTE: If the sensor always registers zero, try switching the static and dynamic tubes. - - - Differential pressure sensor offset - The offset (zero-reading) in Pascal - - - Maximum height above ground when reliant on optical flow - This parameter defines the maximum distance from ground at which the optical flow sensor operates reliably. The height setpoint will be limited to be no greater than this value when the navigation system is completely reliant on optical flow data and the height above ground estimate is valid. The sensor may be usable above this height, but accuracy will progressively degrade. - 1.0 - 100.0 - m - 2 - 0.1 - - - Magnitude of maximum angular flow rate reliably measurable by the optical flow sensor - Optical flow data will not fused by the estimators if the magnitude of the flow rate exceeds this value and control loops will be instructed to limit ground speed such that the flow rate produced by movement over ground is less than 50% of this value. - 1.0 - rad/s - 2 - - - Minimum height above ground when reliant on optical flow - This parameter defines the minimum distance from ground at which the optical flow sensor operates reliably. The sensor may be usable below this height, but accuracy will progressively reduce to loss of focus. - 0.0 - 1.0 - m - 2 - 0.1 - - - - - Enable external ADS1115 ADC - If enabled, the internal ADC is not used. - true - - - Capacity/current multiplier for high-current capable SMBUS battery - 1 - true - - - Battery device model - 0 - 2 - true - - AutoDetect - BQ40Z50 based - BQ40Z80 based - - - - I2C address for BatMon battery 1 - 1 - true - - - Parameter to enable BatMon module - 0 - 2 - true - - Disabled - Start on default I2C addr(BATMON_ADDR_DFLT) - Autodetect I2C address (TODO) - - - - Airspeed sensor compensation model for the SDP3x - Model with Pitot CAL_AIR_TUBED_MM: Not used, 1.5 mm tubes assumed. CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor. Model without Pitot (1.5 mm tubes) CAL_AIR_TUBED_MM: Not used, 1.5 mm tubes assumed. CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor. Tube Pressure Drop CAL_AIR_TUBED_MM: Diameter in mm of the pitot and tubes, must have the same diameter. CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor and the static + dynamic port length of the pitot. - - Model with Pitot - Model without Pitot (1.5 mm tubes) - Tube Pressure Drop - - - - Airspeed sensor tube diameter. Only used for the Tube Pressure Drop Compensation - 1.5 - 100 - mm - - - Airspeed sensor tube length - See the CAL_AIR_CMODEL explanation on how this parameter should be set. - 0.01 - 2.00 - m - - - For legacy QGC support only - Use SENS_MAG_SIDES instead - - - Low pass filter cutoff frequency for accel - The cutoff frequency for the 2nd order butterworth filter on the primary accelerometer. This only affects the signal sent to the controllers, not the estimators. 0 disables the filter. - 0 - 1000 - Hz - true - - - Cutoff frequency for angular acceleration (D-Term filter) - The cutoff frequency for the 2nd order butterworth filter used on the time derivative of the measured angular velocity, also known as the D-term filter in the rate controller. The D-term uses the derivative of the rate and thus is the most susceptible to noise. Therefore, using a D-term filter allows to increase IMU_GYRO_CUTOFF, which leads to reduced control latency and permits to increase the P gains. A value of 0 disables the filter. - 0 - 1000 - Hz - true - - - IMU gyro auto calibration enable - true - - - Low pass filter cutoff frequency for gyro - The cutoff frequency for the 2nd order butterworth filter on the primary gyro. This only affects the angular velocity sent to the controllers, not the estimators. It applies also to the angular acceleration (D-Term filter), see IMU_DGYRO_CUTOFF. A value of 0 disables the filter. - 0 - 1000 - Hz - true - - - IMU gyro ESC notch filter bandwidth - Bandwidth per notch filter when using dynamic notch filtering with ESC RPM. - 5 - 30 - Hz - - - IMU gyro dynamic notch filtering - Enable bank of dynamically updating notch filters. Requires ESC RPM feedback or onboard FFT (IMU_GYRO_FFT_EN). - 0 - 3 - - ESC RPM - FFT - - - - IMU gyro dynamic notch filter harmonics - ESC RPM number of harmonics (multiples of RPM) for ESC RPM dynamic notch filtering. - 1 - 7 - - - IMU gyro dynamic notch filter minimum frequency - Minimum notch filter frequency in Hz. - Hz - - - IMU gyro FFT enable - true - - - IMU gyro FFT length - Hz - true - - 256 - 512 - 1024 - 4096 - - - - IMU gyro FFT maximum frequency - 1 - 1000 - Hz - true + + Magnetometer 0 Y-axis offset + gauss + 3 - - IMU gyro FFT minimum frequency - 1 - 1000 - Hz - true + + Magnetometer 0 Y-axis scaling factor + 0.1 + 3.0 + 3 - - IMU gyro FFT SNR - 1 - 30 + + Magnetometer 0 Z Axis throttle compensation + Coefficient describing linear relationship between +Z component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - Notch filter bandwidth for gyro - The frequency width of the stop band for the 2nd order notch filter on the primary gyro. See "IMU_GYRO_NF0_FRQ" to activate the filter and to set the notch frequency. Applies to both angular velocity and angular acceleration sent to the controllers. - 0 - 100 - Hz - true + + Magnetometer 0 Z-axis off diagonal scale factor - - Notch filter frequency for gyro - The center frequency for the 2nd order notch filter on the primary gyro. This filter can be enabled to avoid feedback amplification of structural resonances at a specific frequency. This only affects the signal sent to the controllers, not the estimators. Applies to both angular velocity and angular acceleration sent to the controllers. See "IMU_GYRO_NF0_BW" to set the bandwidth of the filter. A value of 0 disables the filter. - 0 - 1000 - Hz - true + + Magnetometer 0 Z-axis offset + gauss + 3 - - Notch filter 1 bandwidth for gyro - The frequency width of the stop band for the 2nd order notch filter on the primary gyro. See "IMU_GYRO_NF1_FRQ" to activate the filter and to set the notch frequency. Applies to both angular velocity and angular acceleration sent to the controllers. - 0 - 100 - Hz - true + + Magnetometer 0 Z-axis scaling factor + 0.1 + 3.0 + 3 - - Notch filter 2 frequency for gyro - The center frequency for the 2nd order notch filter on the primary gyro. This filter can be enabled to avoid feedback amplification of structural resonances at a specific frequency. This only affects the signal sent to the controllers, not the estimators. Applies to both angular velocity and angular acceleration sent to the controllers. See "IMU_GYRO_NF1_BW" to set the bandwidth of the filter. A value of 0 disables the filter. - 0 - 1000 - Hz - true + + Magnetometer 1 calibration device ID + Device ID of the magnetometer this calibration applies to. - - Gyro control data maximum publication rate (inner loop rate) - The maximum rate the gyro control data (vehicle_angular_velocity) will be allowed to publish at. This is the loop rate for the rate controller and outputs. Note: sensor data is always read and filtered at the full raw rate (eg commonly 8 kHz) regardless of this setting. - 100 - 2000 - Hz - true - - 100 Hz - 250 Hz - 400 Hz - 800 Hz - 1000 Hz - 2000 Hz - + + Magnetometer 1 Custom Euler Pitch Angle + Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" + -180 + 180 + deg - - IMU integration rate - The rate at which raw IMU data is integrated to produce delta angles and delta velocities. Recommended to set this to a multiple of the estimator update period (currently 10 ms for ekf2). - 100 - 1000 - Hz - true + + Magnetometer 1 priority - 100 Hz - 200 Hz - 250 Hz - 400 Hz + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max - - INA220 Power Monitor Config - 0 - 65535 - 1 - 1 - - - INA220 Power Monitor Battery Max Current - 0.1 - 500.0 - 2 - 0.1 + + Magnetometer 1 Custom Euler Roll Angle + Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" + -180 + 180 + deg - - INA220 Power Monitor Regulator Max Current - 0.1 - 500.0 - 2 - 0.1 + + Magnetometer 1 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. +Set to "Custom Euler Angle" to define the rotation using CAL_MAG1_ROLL, CAL_MAG1_PITCH and CAL_MAG1_YAW. + -1 + 100 + + Internal + No rotation + Yaw 45° + Yaw 90° + Yaw 135° + Yaw 180° + Yaw 225° + Yaw 270° + Yaw 315° + Roll 180° + Roll 180°, Yaw 45° + Roll 180°, Yaw 90° + Roll 180°, Yaw 135° + Pitch 180° + Roll 180°, Yaw 225° + Roll 180°, Yaw 270° + Roll 180°, Yaw 315° + Roll 90° + Roll 90°, Yaw 45° + Roll 90°, Yaw 90° + Roll 90°, Yaw 135° + Roll 270° + Roll 270°, Yaw 45° + Roll 270°, Yaw 90° + Roll 270°, Yaw 135° + Pitch 90° + Pitch 270° + Pitch 180°, Yaw 90° + Pitch 180°, Yaw 270° + Roll 90°, Pitch 90° + Roll 180°, Pitch 90° + Roll 270°, Pitch 90° + Roll 90°, Pitch 180° + Roll 270°, Pitch 180° + Roll 90°, Pitch 270° + Roll 180°, Pitch 270° + Roll 270°, Pitch 270° + Roll 90°, Pitch 180°, Yaw 90° + Roll 90°, Yaw 270° + Roll 90°, Pitch 68°, Yaw 293° + Pitch 315° + Roll 90°, Pitch 315° + Custom Euler Angle + - - INA220 Power Monitor Battery Shunt - 0.000000001 - 0.1 - 10 - .000000001 + + Magnetometer 1 X Axis throttle compensation + Coefficient describing linear relationship between +X component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - INA220 Power Monitor Regulator Shunt - 0.000000001 - 0.1 - 10 - .000000001 + + Magnetometer 1 X-axis off diagonal scale factor + 3 - - INA226 Power Monitor Config - 0 - 65535 - 1 - 1 + + Magnetometer 1 X-axis offset + gauss + 3 - - INA226 Power Monitor Max Current + + Magnetometer 1 X-axis scaling factor 0.1 - 200.0 - 2 - 0.1 + 3.0 + 3 - - INA226 Power Monitor Shunt - 0.000000001 - 0.1 - 10 - .000000001 + + Magnetometer 1 Custom Euler Yaw Angle + Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" + -180 + 180 + deg - - INA228 Power Monitor Config - 0 - 65535 - 1 - 1 + + Magnetometer 1 Y Axis throttle compensation + Coefficient describing linear relationship between +Y component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - INA228 Power Monitor Max Current - 0.1 - 327.68 - 2 - 0.1 + + Magnetometer 1 Y-axis off diagonal scale factor + 3 - - INA228 Power Monitor Shunt - 0.000000001 - 0.1 - 10 - .000000001 + + Magnetometer 1 Y-axis offset + gauss + 3 - - INA238 Power Monitor Max Current + + Magnetometer 1 Y-axis scaling factor 0.1 - 327.68 - 2 - 0.1 - - - INA238 Power Monitor Shunt - 0.000000001 - 0.1 - 10 - .000000001 + 3.0 + 3 - - PCF8583 rotorfreq (i2c) pulse count - Nmumber of signals per rotation of actuator - 1 - true + + Magnetometer 1 Z Axis throttle compensation + Coefficient describing linear relationship between +Z component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - PCF8583 rotorfreq (i2c) pool interval - Determines how often the sensor is read out. - us - true + + Magnetometer 1 Z-axis off diagonal scale factor - - PCF8583 rotorfreq (i2c) pulse reset value - Internal device counter is reset to 0 when overrun this value, counter is able to store up to 6 digits reset of counter takes some time - measurement with reset has worse accuracy. 0 means reset counter after every measurement. - true + + Magnetometer 1 Z-axis offset + gauss + 3 - - AFBR Rangefinder Short/Long Range Threshold Hysteresis - This parameter defines the hysteresis for switching between short and long range mode. - 1 - 10 - m + + Magnetometer 1 Z-axis scaling factor + 0.1 + 3.0 + 3 - - AFBR Rangefinder Long Range Rate - This parameter defines measurement rate of the AFBR Rangefinder in long range mode. - 1 - 100 + + Magnetometer 2 calibration device ID + Device ID of the magnetometer this calibration applies to. - - AFBR Rangefinder Mode - This parameter defines the mode of the AFBR Rangefinder. - 0 - 3 - true + + Magnetometer 2 Custom Euler Pitch Angle + Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" + -180 + 180 + deg + + + Magnetometer 2 priority - Short Range Mode - Long Range Mode - High Speed Short Range Mode - High Speed Long Range Mode + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max - - AFBR Rangefinder Short Range Rate - This parameter defines measurement rate of the AFBR Rangefinder in short range mode. - 1 - 100 - - - AFBR Rangefinder Short/Long Range Threshold - This parameter defines the threshold for switching between short and long range mode. The mode will switch from short to long range when the distance is greater than the threshold plus the hysteresis. The mode will switch from long to short range when the distance is less than the threshold minus the hysteresis. - 1 - 50 - m - - - QNH for barometer - 500 - 1500 - hPa - - - Baro max rate - Barometric air data maximum publication rate. This is an upper bound, actual barometric data rate is still dependent on the sensor. - 1 - 200 - Hz + + Magnetometer 2 Custom Euler Roll Angle + Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" + -180 + 180 + deg - - Board rotation - This parameter defines the rotation of the FMU board relative to the platform. + + Magnetometer 2 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. +Set to "Custom Euler Angle" to define the rotation using CAL_MAG2_ROLL, CAL_MAG2_PITCH and CAL_MAG2_YAW. -1 - 40 - true + 100 + Internal No rotation Yaw 45° Yaw 90° @@ -22404,362 +13755,125 @@ Roll 90°, Pitch 68°, Yaw 293° Pitch 315° Roll 90°, Pitch 315° + Custom Euler Angle - - Board rotation X (Roll) offset - This parameter defines a rotational offset in degrees around the X (Roll) axis It allows the user to fine tune the board offset in the event of misalignment. - deg - - - Board rotation Y (Pitch) offset - This parameter defines a rotational offset in degrees around the Y (Pitch) axis. It allows the user to fine tune the board offset in the event of misalignment. - deg - - - Board rotation Z (YAW) offset - This parameter defines a rotational offset in degrees around the Z (Yaw) axis. It allows the user to fine tune the board offset in the event of misalignment. - deg - - - Serial Configuration for Lanbao PSK-CM8JL65-CC5 - Configure on which serial port to run Lanbao PSK-CM8JL65-CC5. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Distance Sensor Rotation - Distance Sensor Rotation as MAV_SENSOR_ORIENTATION enum - True - - ROTATION_FORWARD_FACING - ROTATION_RIGHT_FACING - ROTATION_BACKWARD_FACING - ROTATION_LEFT_FACING - ROTATION_UPWARD_FACING - ROTATION_DOWNWARD_FACING - - - - Analog Devices ADIS16448 IMU (external SPI) - 0 - 1 - true - - Disabled - Enabled - - - - Analog Devices ADIS16507 IMU (external SPI) - true - - - Simulate Aux Global Position (AGP) - 0 - 1 - true - - Disabled - Enabled - - - - Enable simulated airspeed sensor instance - 0 - 1 - true - - Disabled - Enabled - - - - ASP5033 differential pressure sensor (external I2C) - true - - - Amphenol AUAV differential / absolute pressure sensor (external I2C) - true - - Sensor disabled, when explicitly started treated as AUAV L05D - AUAV L05D - AUAV L10D - AUAV L30D - - - - Enable simulated barometer sensor instance - 0 - 1 - true - - Disabled - Enabled - - - - SMBUS Smart battery driver BQ40Z50 and BQ40Z80 - true - - - Eagle Tree airspeed sensor (external I2C) - true - - - Enable simulated GPS sinstance - 0 - 1 - true - - Disabled - Enabled - - - - Enable INA220 Power Monitor - For systems a INA220 Power Monitor, this should be set to true - true - - - Enable INA226 Power Monitor - For systems a INA226 Power Monitor, this should be set to true - true - - - Enable INA228 Power Monitor - For systems a INA228 Power Monitor, this should be set to true - true - - - Enable INA238 Power Monitor - For systems a INA238 Power Monitor, this should be set to true - true - - - IR-LOCK Sensor (external I2C) - true - - - Lidar-Lite (LL40LS) - 0 - 2 - true - - Disabled - PWM - I2C - - - - Enable simulated magnetometer sensor instance - 0 - 1 - true - - Disabled - Enabled - - - - Maxbotix Sonar (mb12xx) - true - - - Enable Mappydot rangefinder (i2c) - 0 - 1 - true - - Disabled - Autodetect - - - - TE MS4515 differential pressure sensor (external I2C) - true - - - TE MS4525DO differential pressure sensor (external I2C) - true - - - TE MS5525DSO differential pressure sensor (external I2C) - true - - - PAA3905 Optical Flow - true - - - PAW3902/PAW3903 Optical Flow - true - - - PCF8583 eneable driver - Run PCF8583 driver automatically - 0 - 1 - true - - Disabled - Eneabled - + + Magnetometer 2 X Axis throttle compensation + Coefficient describing linear relationship between +X component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - PGA460 Ultrasonic driver (PGA460) - true + + Magnetometer 2 X-axis off diagonal scale factor + 3 - - PMW3901 Optical Flow - true + + Magnetometer 2 X-axis offset + gauss + 3 - - PX4 Flow Optical Flow - true + + Magnetometer 2 X-axis scaling factor + 0.1 + 3.0 + 3 - - Murata SCH16T IMU (external SPI) - 0 - 1 - true - - Disabled - Enabled - + + Magnetometer 2 Custom Euler Yaw Angle + Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" + -180 + 180 + deg - - Sensirion SDP3X differential pressure sensor (external I2C) - true + + Magnetometer 2 Y Axis throttle compensation + Coefficient describing linear relationship between +Y component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - Lightware Laser Rangefinder hardware model (serial) - true - - SF02 - SF10/a - SF10/b - SF10/c - SF11/c - SF30/b - SF30/c - LW20/c - + + Magnetometer 2 Y-axis off diagonal scale factor + 3 - - Lightware SF1xx/SF20/LW20 laser rangefinder (i2c) - 0 - 6 - true - - Disabled - SF10/a - SF10/b - SF10/c - SF11/c - SF/LW20/b - SF/LW20/c - SF/LW30/d - - - - Serial Configuration for Lightware SF45 Rangefinder (serial) - Configure on which serial port to run Lightware SF45 Rangefinder (serial). - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - SHT3x temperature and hygrometer - true + + Magnetometer 2 Y-axis offset + gauss + 3 - - Goertek SPA06 Barometer (external I2C) - true + + Magnetometer 2 Y-axis scaling factor + 0.1 + 3.0 + 3 - - Goertek SPL06 Barometer (external I2C) - true + + Magnetometer 2 Z Axis throttle compensation + Coefficient describing linear relationship between +Z component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - HY-SRF05 / HC-SR05 - true + + Magnetometer 2 Z-axis off diagonal scale factor - - TF02 Pro Distance Sensor (i2c) - true + + Magnetometer 2 Z-axis offset + gauss + 3 - - Thermal control of sensor temperature - - Thermal control unavailable - Thermal control off - Thermal control enabled - + + Magnetometer 2 Z-axis scaling factor + 0.1 + 3.0 + 3 - - TeraRanger Rangefinder (i2c) - 0 - 3 - true + + Magnetometer 3 calibration device ID + Device ID of the magnetometer this calibration applies to. + + + Magnetometer 3 Custom Euler Pitch Angle + Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + -180 + 180 + deg + + + Magnetometer 3 priority + Uninitialized Disabled - Autodetect - TROne - TREvo60m - TREvo600Hz - TREvo3m + Min + Low + Medium (Default) + High + Max - - VL53L0X Distance Sensor - true - - - VL53L1X Distance Sensor - true - - - External I2C probe - Probe for optional external I2C devices. - - - Optical flow max rate - Optical flow data maximum publication rate. This is an upper bound, actual optical flow data rate is still dependent on the sensor. - 1 - 200 - Hz - true + + Magnetometer 3 Custom Euler Roll Angle + Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + -180 + 180 + deg - - Optical flow rotation - This parameter defines the yaw rotation of the optical flow relative to the vehicle body frame. Zero rotation is defined as X on flow board pointing towards front of vehicle. + + Magnetometer 3 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. +Set to "Custom Euler Angle" to define the rotation using CAL_MAG3_ROLL, CAL_MAG3_PITCH and CAL_MAG3_YAW. + -1 + 100 + Internal No rotation Yaw 45° Yaw 90° @@ -22768,421 +13882,432 @@ Yaw 225° Yaw 270° Yaw 315° + Roll 180° + Roll 180°, Yaw 45° + Roll 180°, Yaw 90° + Roll 180°, Yaw 135° + Pitch 180° + Roll 180°, Yaw 225° + Roll 180°, Yaw 270° + Roll 180°, Yaw 315° + Roll 90° + Roll 90°, Yaw 45° + Roll 90°, Yaw 90° + Roll 90°, Yaw 135° + Roll 270° + Roll 270°, Yaw 45° + Roll 270°, Yaw 90° + Roll 270°, Yaw 135° + Pitch 90° + Pitch 270° + Pitch 180°, Yaw 90° + Pitch 180°, Yaw 270° + Roll 90°, Pitch 90° + Roll 180°, Pitch 90° + Roll 270°, Pitch 90° + Roll 90°, Pitch 180° + Roll 270°, Pitch 180° + Roll 90°, Pitch 270° + Roll 180°, Pitch 270° + Roll 270°, Pitch 270° + Roll 90°, Pitch 180°, Yaw 90° + Roll 90°, Yaw 270° + Roll 90°, Pitch 68°, Yaw 293° + Pitch 315° + Roll 90°, Pitch 315° + Custom Euler Angle - - Optical flow scale factor - 0.5 - 1.5 - 2 - - - Serial Configuration for FT Technologies Digital Wind Sensor (serial) - Configure on which serial port to run FT Technologies Digital Wind Sensor (serial). - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - + + Magnetometer 3 X Axis throttle compensation + Coefficient describing linear relationship between +X component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - Multi GPS Blending Control Mask - Set bits in the following positions to set which GPS accuracy metrics will be used to calculate the blending weight. Set to zero to disable and always used first GPS instance. 0 : Set to true to use speed accuracy 1 : Set to true to use horizontal position accuracy 2 : Set to true to use vertical position accuracy - 0 - 7 - - use speed accuracy - use hpos accuracy - use vpos accuracy - + + Magnetometer 3 X-axis off diagonal scale factor + 3 - - Multi GPS primary instance - When no blending is active, this defines the preferred GPS receiver instance. The GPS selection logic waits until the primary receiver is available to send data to the EKF even if a secondary instance is already available. The secondary instance is then only used if the primary one times out. To have an equal priority of all the instances, set this parameter to -1 and the best receiver will be used. This parameter has no effect if blending is active. - -1 - 1 + + Magnetometer 3 X-axis offset + gauss + 3 - - Multi GPS Blending Time Constant - Sets the longest time constant that will be applied to the calculation of GPS position and height offsets used to correct data from multiple GPS data for steady state position differences. - 1.0 - 100.0 - s - 1 + + Magnetometer 3 X-axis scaling factor + 0.1 + 3.0 + 3 - - IMU auto calibration - Automatically initialize IMU (accel/gyro) calibration from bias estimates if available. + + Magnetometer 3 Custom Euler Yaw Angle + Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + -180 + 180 + deg - - IMU notify clipping - Notify the user if the IMU is clipping + + Magnetometer 3 Y Axis throttle compensation + Coefficient describing linear relationship between +Y component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - Sensors hub IMU mode - true - - Disabled - Publish primary IMU selection - + + Magnetometer 3 Y-axis off diagonal scale factor + 3 - - Target IMU temperature - 0 - 85.0 - celcius + + Magnetometer 3 Y-axis offset + gauss 3 - - IMU heater controller feedforward value - 0 - 1.0 - % + + Magnetometer 3 Y-axis scaling factor + 0.1 + 3.0 3 - - IMU heater controller integrator gain value - 0 - 1.0 - us/C + + Magnetometer 3 Z Axis throttle compensation + Coefficient describing linear relationship between +Z component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] 3 - - IMU heater controller proportional gain value - 0 - 2.0 - us/C + + Magnetometer 3 Z-axis off diagonal scale factor + + + Magnetometer 3 Z-axis offset + gauss 3 - - Enable internal barometers - For systems with an external barometer, this should be set to false to make sure that the external is used. - true + + Magnetometer 3 Z-axis scaling factor + 0.1 + 3.0 + 3 - - Serial Configuration for LeddarOne Rangefinder - Configure on which serial port to run LeddarOne Rangefinder. - true + + Type of magnetometer compensation Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 + Throttle-based compensation + Current-based compensation (battery_status instance 0) + Current-based compensation (battery_status instance 1) - - Magnetometer auto calibration - Automatically initialize magnetometer calibration from bias estimate if available. + + Differential pressure sensor analog scaling + Pick the appropriate scaling from the datasheet. +this number defines the (linear) conversion from voltage +to Pascal (pa). For the MPXV7002DP this is 1000. +NOTE: If the sensor always registers zero, try switching +the static and dynamic tubes. - - Automatically set external rotations - During calibration attempt to automatically determine the rotation of external magnetometers. + + Differential pressure sensor offset + The offset (zero-reading) in Pascal - - Sensors hub mag mode - true - - Publish all magnetometers - Publish primary magnetometer - + + Reverse differential pressure sensor readings + Reverse the raw measurements of all differential pressure sensors. +This can be enabled if the sensors have static and dynamic ports swapped. - - Magnetometer max rate - Magnetometer data maximum publication rate. This is an upper bound, actual magnetometer data rate is still dependent on the sensor. - 1 - 200 - Hz - true + + Maximum height above ground when reliant on optical flow + This parameter defines the maximum distance from ground at which the optical flow sensor operates reliably. +The height setpoint will be limited to be no greater than this value when the navigation system +is completely reliant on optical flow data and the height above ground estimate is valid. +The sensor may be usable above this height, but accuracy will progressively degrade. + 1.0 + 100.0 + m + 2 + 0.1 - - Bitfield selecting mag sides for calibration - If set to two side calibration, only the offsets are estimated, the scale calibration is left unchanged. Thus an initial six side calibration is recommended. Bits: ORIENTATION_TAIL_DOWN = 1 ORIENTATION_NOSE_DOWN = 2 ORIENTATION_LEFT = 4 ORIENTATION_RIGHT = 8 ORIENTATION_UPSIDE_DOWN = 16 ORIENTATION_RIGHTSIDE_UP = 32 - 34 - 63 - - Two side calibration - Three side calibration - Six side calibration - + + Magnitude of maximum angular flow rate reliably measurable by the optical flow sensor + Optical flow data will not fused by the estimators if the magnitude of the flow rate exceeds this value and +control loops will be instructed to limit ground speed such that the flow rate produced by movement over ground +is less than 50% of this value. + 1.0 + rad/s + 2 - - MaxBotix MB12XX Sensor 0 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + + Minimum height above ground when reliant on optical flow + This parameter defines the minimum distance from ground at which the optical flow sensor operates reliably. +The sensor may be usable below this height, but accuracy will progressively reduce to loss of focus. + 0.0 + 1.0 + m + 2 + 0.1 - - MaxBotix MB12XX Sensor 10 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 - true + + + + Airspeed sensor compensation model for the SDP3x + Model with Pitot +CAL_AIR_TUBED_MM: Not used, 1.5 mm tubes assumed. +CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor. +Model without Pitot (1.5 mm tubes) +CAL_AIR_TUBED_MM: Not used, 1.5 mm tubes assumed. +CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor. +Tube Pressure Drop +CAL_AIR_TUBED_MM: Diameter in mm of the pitot and tubes, must have the same diameter. +CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor and the static + dynamic port length of the pitot. - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Model with Pitot + Model without Pitot (1.5 mm tubes) + Tube Pressure Drop - - MaxBotix MB12XX Sensor 12 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + + Airspeed sensor tube diameter. Only used for the Tube Pressure Drop Compensation + 1.5 + 100 + mm + + + Airspeed sensor tube length + See the CAL_AIR_CMODEL explanation on how this parameter should be set. + 0.01 + 2.00 + m + + + For legacy QGC support only + Use SENS_MAG_SIDES instead - - MaxBotix MB12XX Sensor 1 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + Low pass filter cutoff frequency for accel + The cutoff frequency for the 2nd order butterworth filter on the primary accelerometer. +This only affects the signal sent to the controllers, not the estimators. 0 disables the filter. 0 - 7 + 1000 + Hz true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 2 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + Cutoff frequency for angular acceleration (D-Term filter) + The cutoff frequency for the 2nd order butterworth filter used on +the time derivative of the measured angular velocity, also known as +the D-term filter in the rate controller. The D-term uses the derivative of +the rate and thus is the most susceptible to noise. Therefore, using +a D-term filter allows to increase IMU_GYRO_CUTOFF, which +leads to reduced control latency and permits to increase the P gains. +A value of 0 disables the filter. 0 - 7 + 1000 + Hz + 1 + 0.1 + false + + + IMU gyro auto calibration enable true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 3 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + Low pass filter cutoff frequency for gyro + The cutoff frequency for the 2nd order butterworth filter on the primary gyro. +This only affects the angular velocity sent to the controllers, not the estimators. +It applies also to the angular acceleration (D-Term filter), see IMU_DGYRO_CUTOFF. +A value of 0 disables the filter. 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + 1000 + Hz + 1 + 0.1 + false + + + IMU gyro ESC notch filter bandwidth + Bandwidth per notch filter when using dynamic notch filtering with ESC RPM. + 5 + 30 + Hz + 1 + 0.1 - - MaxBotix MB12XX Sensor 4 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + IMU gyro dynamic notch filtering + Enable bank of dynamically updating notch filters. +Requires ESC RPM feedback or onboard FFT (IMU_GYRO_FFT_EN). 0 + 3 + + ESC RPM + FFT + + + + IMU gyro dynamic notch filter harmonics + ESC RPM number of harmonics (multiples of RPM) for ESC RPM dynamic notch filtering. + 1 7 + + + IMU gyro dynamic notch filter minimum frequency + Minimum notch filter frequency in Hz. + Hz + 1 + 0.1 + + + IMU gyro FFT enable true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 5 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 + + IMU gyro FFT length + Hz true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + 256 + 512 + 1024 + 4096 - - MaxBotix MB12XX Sensor 6 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 + + IMU gyro FFT maximum frequency + 1 + 1000 + Hz true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 7 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 + + IMU gyro FFT minimum frequency + 1 + 1000 + Hz true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 8 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + IMU gyro FFT SNR + 1 + 30 + + + Notch filter bandwidth for gyro + The frequency width of the stop band for the 2nd order notch filter on the primary gyro. +See "IMU_GYRO_NF0_FRQ" to activate the filter and to set the notch frequency. +Applies to both angular velocity and angular acceleration sent to the controllers. 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + 100 + Hz + 1 + 0.1 + false - - MaxBotix MB12XX Sensor 9 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + Notch filter frequency for gyro + The center frequency for the 2nd order notch filter on the primary gyro. +This filter can be enabled to avoid feedback amplification of structural resonances at a specific frequency. +This only affects the signal sent to the controllers, not the estimators. +Applies to both angular velocity and angular acceleration sent to the controllers. +See "IMU_GYRO_NF0_BW" to set the bandwidth of the filter. +A value of 0 disables the filter. 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + 1000 + Hz + 1 + 0.1 + false - - MappyDot Sensor 0 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Notch filter 1 bandwidth for gyro + The frequency width of the stop band for the 2nd order notch filter on the primary gyro. +See "IMU_GYRO_NF1_FRQ" to activate the filter and to set the notch frequency. +Applies to both angular velocity and angular acceleration sent to the controllers. 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + 100 + Hz + 1 + 0.1 + false - - MappyDot Sensor 10 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Notch filter 2 frequency for gyro + The center frequency for the 2nd order notch filter on the primary gyro. +This filter can be enabled to avoid feedback amplification of structural resonances at a specific frequency. +This only affects the signal sent to the controllers, not the estimators. +Applies to both angular velocity and angular acceleration sent to the controllers. +See "IMU_GYRO_NF1_BW" to set the bandwidth of the filter. +A value of 0 disables the filter. 0 - 7 + 1000 + Hz + 1 + 0.1 + false + + + Gyro control data maximum publication rate (inner loop rate) + The maximum rate the gyro control data (vehicle_angular_velocity) will be +allowed to publish at. This is the loop rate for the rate controller and outputs. +Note: sensor data is always read and filtered at the full raw rate (eg commonly 8 kHz) regardless of this setting. + 100 + 2000 + Hz true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + 100 Hz + 250 Hz + 400 Hz + 800 Hz + 1000 Hz + 2000 Hz - - MappyDot Sensor 12 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. - 0 - 7 + + IMU integration rate + The rate at which raw IMU data is integrated to produce delta angles and delta velocities. +Recommended to set this to a multiple of the estimator update period (currently 10 ms for ekf2). + 100 + 1000 + Hz true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + 100 Hz + 200 Hz + 250 Hz + 400 Hz - - MappyDot Sensor 1 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. - 0 - 7 + + QNH for barometer + 500 + 1500 + hPa + + + Baro max rate + Barometric air data maximum publication rate. This is an upper bound, +actual barometric data rate is still dependent on the sensor. + 1 + 200 + Hz + + + Barometer auto calibration + Automatically calibrate barometer based on the GNSS height + + + Board rotation + This parameter defines the rotation of the FMU board relative to the platform. + -1 + 40 true No rotation @@ -23193,116 +14318,145 @@ Yaw 225° Yaw 270° Yaw 315° + Roll 180° + Roll 180°, Yaw 45° + Roll 180°, Yaw 90° + Roll 180°, Yaw 135° + Pitch 180° + Roll 180°, Yaw 225° + Roll 180°, Yaw 270° + Roll 180°, Yaw 315° + Roll 90° + Roll 90°, Yaw 45° + Roll 90°, Yaw 90° + Roll 90°, Yaw 135° + Roll 270° + Roll 270°, Yaw 45° + Roll 270°, Yaw 90° + Roll 270°, Yaw 135° + Pitch 90° + Pitch 270° + Pitch 180°, Yaw 90° + Pitch 180°, Yaw 270° + Roll 90°, Pitch 90° + Roll 180°, Pitch 90° + Roll 270°, Pitch 90° + Roll 90°, Pitch 180° + Roll 270°, Pitch 180° + Roll 90°, Pitch 270° + Roll 180°, Pitch 270° + Roll 270°, Pitch 270° + Roll 90°, Pitch 180°, Yaw 90° + Roll 90°, Yaw 270° + Roll 90°, Pitch 68°, Yaw 293° + Pitch 315° + Roll 90°, Pitch 315° - - MappyDot Sensor 2 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Board rotation X (roll) offset + Rotation from flight controller board to vehicle body frame. +This parameter gets set during the "level horizon" calibration or can be +set manually. + -45.0 + 45.0 + deg + 1 + + + Board rotation Y (pitch) offset + Rotation from flight controller board to vehicle body frame. +This parameter gets set during the "level horizon" calibration or can be +set manually. + -45.0 + 45.0 + deg + 1 + + + Board rotation Z (yaw) offset + Rotation from flight controller board to vehicle body frame. +Has to be set manually (not set by any calibration). + -45.0 + 45.0 + deg + 1 + + + Simulate Aux Global Position (AGP) 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 3 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Enable simulated airspeed sensor instance 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 4 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Enable simulated barometer sensor instance 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 5 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Enable simulated GPS sinstance 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 6 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Enable simulated magnetometer sensor instance 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 7 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. - 0 - 7 - true + + Thermal control of sensor temperature - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Thermal control unavailable + Thermal control off + Thermal control enabled - - MappyDot Sensor 8 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. - 0 - 7 + + External I2C probe + Probe for optional external I2C devices. + + + Optical flow max rate + Optical flow data maximum publication rate. This is an upper bound, +actual optical flow data rate is still dependent on the sensor. + 1 + 200 + Hz true + + + Optical flow rotation + This parameter defines the yaw rotation of the optical flow relative to the vehicle body frame. +Zero rotation is defined as X on flow board pointing towards front of vehicle. No rotation Yaw 45° @@ -23314,168 +14468,113 @@ Yaw 315° - - MappyDot Sensor 9 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Optical flow scale factor + 0.5 + 1.5 + 2 + + + Multi GPS Blending Control Mask + Set bits in the following positions to set which GPS accuracy metrics will be used to calculate the blending weight. Set to zero to disable and always used first GPS instance. +0 : Set to true to use speed accuracy +1 : Set to true to use horizontal position accuracy +2 : Set to true to use vertical position accuracy 0 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + + use speed accuracy + use hpos accuracy + use vpos accuracy + + + + Multi GPS primary instance + When no blending is active, this defines the preferred GPS receiver instance. +The GPS selection logic waits until the primary receiver is available to +send data to the EKF even if a secondary instance is already available. +The secondary instance is then only used if the primary one times out. +Accepted values: +-1 : Auto (equal priority for all instances) +0 : Main serial GPS instance +1 : Secondary serial GPS instance +2-127 : UAVCAN module node ID +This parameter has no effect if blending is active. + -1 + 127 + + + Multi GPS Blending Time Constant + Sets the longest time constant that will be applied to the calculation of GPS position and height offsets used to correct data from multiple GPS data for steady state position differences. + 1.0 + 100.0 + s + 1 + + + IMU auto calibration + Automatically initialize IMU (accel/gyro) calibration from bias estimates if available. - - Analog Devices ADIS16448 IMU Orientation(external SPI) - 0 - 101 - true - - ROTATION_NONE - ROTATION_YAW_180 - + + IMU notify clipping + Notify the user if the IMU is clipping - - Serial Configuration for Lightware Laser Rangefinder (serial) - Configure on which serial port to run Lightware Laser Rangefinder (serial). - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Target IMU device ID to regulate temperature - - - Serial Configuration for ThoneFlow-3901U optical flow sensor - Configure on which serial port to run ThoneFlow-3901U optical flow sensor. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for Benewake TFmini Rangefinder - Configure on which serial port to run Benewake TFmini Rangefinder. + + Sensors hub IMU mode true Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for uLanding Radar - Configure on which serial port to run uLanding Radar. + Publish primary IMU selection + + + + Enable internal barometers + For systems with an external barometer, this should be set to false to make sure that the external is used. true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for VectorNav (VN-100, VN-200, VN-300) - Configure on which serial port to run VectorNav (VN-100, VN-200, VN-300). + + + Magnetometer auto calibration + Automatically initialize magnetometer calibration from bias estimate if available. + + + Automatically set external rotations + During calibration attempt to automatically determine the rotation of external magnetometers. + + + Sensors hub mag mode true - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Orientation upright or facing downward - The SF45 mounted facing upward or downward on the frame - True - - Rotation upward - Rotation downward + Publish all magnetometers + Publish primary magnetometer - - Update rate in Hz - The SF45 sets the update rate in Hz to allow greater resolution - True - - 50hz - 100hz - 200hz - 400hz - 500hz - 625hz - 1000hz - 1250hz - 1538hz - 2000hz - 2500hz - 5000hz - - - - Sensor facing forward or backward - The usb port on the sensor indicates 180deg, opposite usb is forward facing - True + + Magnetometer max rate + Magnetometer data maximum publication rate. This is an upper bound, +actual magnetometer data rate is still dependent on the sensor. + 1 + 200 + Hz + true + + + Bitfield selecting mag sides for calibration + If set to two side calibration, only the offsets are estimated, the scale +calibration is left unchanged. Thus an initial six side calibration is +recommended. +Bits: +ORIENTATION_TAIL_DOWN = 1 +ORIENTATION_NOSE_DOWN = 2 +ORIENTATION_LEFT = 4 +ORIENTATION_RIGHT = 8 +ORIENTATION_UPSIDE_DOWN = 16 +ORIENTATION_RIGHTSIDE_UP = 32 + 34 + 63 - Rotation forward - Rotation right - Rotation backward - Rotation left + Two side calibration + Three side calibration + Six side calibration @@ -23488,40 +14587,19 @@ Enabled - - VectorNav driver mode - INS or sensors - - Sensors Only (default) - INS - - - - VOXL Power Monitor Shunt, Battery - 0.000000001 - 0.1 - 10 - .000000001 - true - - - VOXL Power Monitor Shunt, Regulator - 0.000000001 - 0.1 - 10 - .000000001 - true - Toggle automatic receiver configuration - By default, the receiver is automatically configured. Sometimes it may be used for multiple purposes. If the offered parameters aren't sufficient, this parameter can be disabled to have full control of the receiver configuration. A good way to use this is to enable automatic configuration, let the receiver be configured, and then disable it to make manual adjustments. + By default, the receiver is automatically configured. Sometimes it may be used for multiple purposes. +If the offered parameters aren't sufficient, this parameter can be disabled to have full control of the receiver configuration. +A good way to use this is to enable automatic configuration, let the receiver be configured, and then disable it to make manual adjustments. True Usage of different constellations - Choice of which constellations the receiver should use for PVT computation. When this is 0, the constellation usage isn't changed. + Choice of which constellations the receiver should use for PVT computation. +When this is 0, the constellation usage isn't changed. 0 63 True @@ -23535,7 +14613,8 @@ Log GPS communication data - Log raw communication between the driver and connected receivers. For example, "To receiver" will log all commands and corrections sent by the driver to the receiver. + Log raw communication between the driver and connected receivers. +For example, "To receiver" will log all commands and corrections sent by the driver to the receiver. 0 3 @@ -23547,7 +14626,9 @@ Setup and expected use of the hardware - Setup and expected use of the hardware. - Default: Use two receivers as completely separate instances. - Moving base: Use two receivers in a rover & moving base setup for heading. + Setup and expected use of the hardware. +- Default: Use two receivers as completely separate instances. +- Moving base: Use two receivers in a rover & moving base setup for heading. 0 1 True @@ -23609,51 +14690,17 @@ Pitch offset for dual antenna GPS - Vertical offsets can be compensated for by adjusting the Pitch offset. Note that this can be interpreted as the "roll" angle in case the antennas are aligned along the perpendicular axis. This occurs in situations where the two antenna ARPs may not be exactly at the same height in the vehicle reference frame. Since pitch is defined as the right-handed rotation about the vehicle Y axis, a situation where the main antenna is mounted lower than the aux antenna (assuming the default antenna setup) will result in a positive pitch. + Vertical offsets can be compensated for by adjusting the Pitch offset. +Note that this can be interpreted as the "roll" angle in case the antennas are aligned along the perpendicular axis. +This occurs in situations where the two antenna ARPs may not be exactly at the same height in the vehicle reference frame. +Since pitch is defined as the right-handed rotation about the vehicle Y axis, +a situation where the main antenna is mounted lower than the aux antenna (assuming the default antenna setup) will result in a positive pitch. -90 90 deg 3 True - - Serial Configuration for GPS Port - Configure on which serial port to run GPS Port. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for Secondary GPS port - Configure on which serial port to run Secondary GPS port. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - Enable sat info Enable publication of satellite info (ORB_ID(satellite_info)) if possible. @@ -23661,21 +14708,28 @@ Logging stream used during automatic configuration - The stream the autopilot sets up on the receiver to output the logging data. Set this to another value if the default stream is already used for another purpose. + The stream the autopilot sets up on the receiver to output the logging data. +Set this to another value if the default stream is already used for another purpose. 1 10 True Main stream used during automatic configuration - The stream the autopilot sets up on the receiver to output the main data. Set this to another value if the default stream is already used for another purpose. + The stream the autopilot sets up on the receiver to output the main data. +Set this to another value if the default stream is already used for another purpose. 1 10 True Heading/Yaw offset for dual antenna GPS - Heading offset angle for dual antenna GPS setups that support heading estimation. Set this to 0 if the antennas are parallel to the forward-facing direction of the vehicle and the rover antenna is in front. The offset angle increases clockwise. Set this to 90 if the rover antenna is placed on the right side of the vehicle and the moving base antenna is on the left side. + Heading offset angle for dual antenna GPS setups that support heading estimation. +Set this to 0 if the antennas are parallel to the forward-facing direction +of the vehicle and the rover antenna is in front. +The offset angle increases clockwise. +Set this to 90 if the rover antenna is placed on the +right side of the vehicle and the moving base antenna is on the left side. -360 360 deg @@ -23683,486 +14737,6 @@ True - - - Serial Configuration for CRSF RC Input Driver - Configure on which serial port to run CRSF RC Input Driver. Crossfire RC (CRSF) driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for DSM RC Input Driver - Configure on which serial port to run DSM RC Input Driver. DSM RC (Spektrum) driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for GHST RC Input Driver - Configure on which serial port to run GHST RC Input Driver. Ghost (GHST) RC driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for RC Input Driver - Configure on which serial port to run RC Input Driver. Setting this to 'Disabled' will use a board-specific default port for RC input. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for SBUS RC Input Driver - Configure on which serial port to run SBUS RC Input Driver. SBUS RC driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Baudrate for the EXT2 Serial Port - Configure the Baudrate for the EXT2 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the GPS 1 Serial Port - Configure the Baudrate for the GPS 1 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the GPS 2 Serial Port - Configure the Baudrate for the GPS 2 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the GPS 3 Serial Port - Configure the Baudrate for the GPS 3 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - MXS Serial Communication Baud rate - Baudrate for the Serial Port connected to the MXS Transponder - 0 - 10 - true - - 38400 - 600 - 4800 - 9600 - RESERVED - 57600 - 115200 - 230400 - 19200 - 460800 - 921600 - - - - Baudrate for the Radio Controller Serial Port - Configure the Baudrate for the Radio Controller Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the TELEM 1 Serial Port - Configure the Baudrate for the TELEM 1 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the TELEM 2 Serial Port - Configure the Baudrate for the TELEM 2 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the TELEM 3 Serial Port - Configure the Baudrate for the TELEM 3 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the TELEM/SERIAL 4 Serial Port - Configure the Baudrate for the TELEM/SERIAL 4 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the UART 6 Serial Port - Configure the Baudrate for the UART 6 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the Wifi Port Serial Port - Configure the Baudrate for the Wifi Port Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - distance sensor maximum range @@ -24187,7 +14761,8 @@ Vehicle inertia about X axis - The inertia is a 3 by 3 symmetric matrix. It represents the difficulty of the vehicle to modify its angular rate. + The inertia is a 3 by 3 symmetric matrix. +It represents the difficulty of the vehicle to modify its angular rate. 0.0 kg m^2 3 @@ -24195,21 +14770,24 @@ Vehicle cross term inertia xy - The inertia is a 3 by 3 symmetric matrix. This value can be set to 0 for a quad symmetric about its center of mass. + The inertia is a 3 by 3 symmetric matrix. +This value can be set to 0 for a quad symmetric about its center of mass. kg m^2 3 0.005 Vehicle cross term inertia xz - The inertia is a 3 by 3 symmetric matrix. This value can be set to 0 for a quad symmetric about its center of mass. + The inertia is a 3 by 3 symmetric matrix. +This value can be set to 0 for a quad symmetric about its center of mass. kg m^2 3 0.005 Vehicle inertia about Y axis - The inertia is a 3 by 3 symmetric matrix. It represents the difficulty of the vehicle to modify its angular rate. + The inertia is a 3 by 3 symmetric matrix. +It represents the difficulty of the vehicle to modify its angular rate. 0.0 kg m^2 3 @@ -24217,14 +14795,16 @@ Vehicle cross term inertia yz - The inertia is a 3 by 3 symmetric matrix. This value can be set to 0 for a quad symmetric about its center of mass. + The inertia is a 3 by 3 symmetric matrix. +This value can be set to 0 for a quad symmetric about its center of mass. kg m^2 3 0.005 Vehicle inertia about Z axis - The inertia is a 3 by 3 symmetric matrix. It represents the difficulty of the vehicle to modify its angular rate. + The inertia is a 3 by 3 symmetric matrix. +It represents the difficulty of the vehicle to modify its angular rate. 0.0 kg m^2 3 @@ -24232,7 +14812,10 @@ First order drag coefficient - Physical coefficient representing the friction with air particules. The greater this value, the slower the quad will move. Drag force function of velocity: D=-KDV*V. The maximum freefall velocity can be computed as V=10*MASS/KDV [m/s] + Physical coefficient representing the friction with air particules. +The greater this value, the slower the quad will move. +Drag force function of velocity: D=-KDV*V. +The maximum freefall velocity can be computed as V=10*MASS/KDV [m/s] 0.0 N/(m/s) 2 @@ -24240,7 +14823,10 @@ First order angular damper coefficient - Physical coefficient representing the friction with air particules during rotations. The greater this value, the slower the quad will rotate. Aerodynamic moment function of body rate: Ma=-KDW*W_B. This value can be set to 0 if unknown. + Physical coefficient representing the friction with air particules during rotations. +The greater this value, the slower the quad will rotate. +Aerodynamic moment function of body rate: Ma=-KDW*W_B. +This value can be set to 0 if unknown. 0.0 Nm/(rad/s) 3 @@ -24248,7 +14834,11 @@ Initial AMSL ground altitude - This value represents the Above Mean Sea Level (AMSL) altitude where the simulation begins. If using FlightGear as a visual animation, this value can be tweaked such that the vehicle lies on the ground at takeoff. LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others to represent a physical ground location on Earth. + This value represents the Above Mean Sea Level (AMSL) altitude where the simulation begins. +If using FlightGear as a visual animation, +this value can be tweaked such that the vehicle lies on the ground at takeoff. +LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others +to represent a physical ground location on Earth. -420.0 8848.0 m @@ -24257,21 +14847,27 @@ Initial geodetic latitude - This value represents the North-South location on Earth where the simulation begins. LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others to represent a physical ground location on Earth. + This value represents the North-South location on Earth where the simulation begins. +LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others +to represent a physical ground location on Earth. -90 90 deg Initial geodetic longitude - This value represents the East-West location on Earth where the simulation begins. LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others to represent a physical ground location on Earth. + This value represents the East-West location on Earth where the simulation begins. +LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others +to represent a physical ground location on Earth. -180 180 deg Pitch arm length - This is the arm length generating the pitching moment This value can be measured with a ruler. This corresponds to half the distance between the front and rear motors. + This is the arm length generating the pitching moment +This value can be measured with a ruler. +This corresponds to half the distance between the front and rear motors. 0.0 m 2 @@ -24279,7 +14875,9 @@ Roll arm length - This is the arm length generating the rolling moment This value can be measured with a ruler. This corresponds to half the distance between the left and right motors. + This is the arm length generating the rolling moment +This value can be measured with a ruler. +This corresponds to half the distance between the left and right motors. 0.0 m 2 @@ -24295,7 +14893,9 @@ Max propeller torque - This is the maximum torque delivered by one propeller when the motor is running at full speed. This value is usually about few percent of the maximum thrust force. + This is the maximum torque delivered by one propeller +when the motor is running at full speed. +This value is usually about few percent of the maximum thrust force. 0.0 Nm 3 @@ -24303,7 +14903,9 @@ Max propeller thrust force - This is the maximum force delivered by one propeller when the motor is running at full speed. This value is usually about 5 times the mass of the quadrotor. + This is the maximum force delivered by one propeller +when the motor is running at full speed. +This value is usually about 5 times the mass of the quadrotor. 0.0 N 2 @@ -24318,16 +14920,28 @@ Vehicle type true - Multicopter + Quadcopter Fixed-Wing Tailsitter + Standard VTOL + Hexacopter + Rover Ackermann + + Wind velocity from east direction + m/s + + + Wind velocity from north direction + m/s + AGP failure mode - Stuck: freeze the measurement to the current location Drift: add a linearly growing bias to the sensor data + Stuck: freeze the measurement to the current location +Drift: add a linearly growing bias to the sensor data 0 3 @@ -24363,7 +14977,9 @@ Automatically configure default values - Set to 1 to reset parameters on next system startup (setting defaults). Platform-specific values are used if available. RC* parameters are preserved. + Set to 1 to reset parameters on next system startup (setting defaults). +Platform-specific values are used if available. +RC* parameters are preserved. Keep parameters Reset parameters to airframe defaults @@ -24378,30 +14994,49 @@ Bootloader update - If enabled, update the bootloader on the next boot. WARNING: do not cut the power during an update process, otherwise you will have to recover using some alternative method (e.g. JTAG). Instructions: - Insert an SD card - Enable this parameter - Reboot the board (plug the power or send a reboot command) - Wait until the board comes back up (or at least 2 minutes) - If it does not come back, check the file bootlog.txt on the SD card + If enabled, update the bootloader on the next boot. +WARNING: do not cut the power during an update process, otherwise you will +have to recover using some alternative method (e.g. JTAG). +Instructions: +- Insert an SD card +- Enable this parameter +- Reboot the board (plug the power or send a reboot command) +- Wait until the board comes back up (or at least 2 minutes) +- If it does not come back, check the file bootlog.txt on the SD card true Enable auto start of accelerometer thermal calibration at the next power up - 0 : Set to 0 to do nothing 1 : Set to 1 to start a calibration at next boot This parameter is reset to zero when the temperature calibration starts. default (0, no calibration) + 0 : Set to 0 to do nothing +1 : Set to 1 to start a calibration at next boot +This parameter is reset to zero when the temperature calibration starts. +default (0, no calibration) 0 1 Enable auto start of barometer thermal calibration at the next power up - 0 : Set to 0 to do nothing 1 : Set to 1 to start a calibration at next boot This parameter is reset to zero when the temperature calibration starts. default (0, no calibration) + 0 : Set to 0 to do nothing +1 : Set to 1 to start a calibration at next boot +This parameter is reset to zero when the temperature calibration starts. +default (0, no calibration) 0 1 Enable auto start of rate gyro thermal calibration at the next power up - 0 : Set to 0 to do nothing 1 : Set to 1 to start a calibration at next boot This parameter is reset to zero when the temperature calibration starts. default (0, no calibration) + 0 : Set to 0 to do nothing +1 : Set to 1 to start a calibration at next boot +This parameter is reset to zero when the temperature calibration starts. +default (0, no calibration) 0 1 Required temperature rise during thermal calibration - A temperature increase greater than this value is required during calibration. Calibration will complete for each sensor when the temperature increase above the starting temperature exceeds the value set by SYS_CAL_TDEL. If the temperature rise is insufficient, the calibration will continue indefinitely and the board will need to be repowered to exit. + A temperature increase greater than this value is required during calibration. +Calibration will complete for each sensor when the temperature increase above the starting temperature exceeds the value set by SYS_CAL_TDEL. +If the temperature rise is insufficient, the calibration will continue indefinitely and the board will need to be repowered to exit. 10 celcius @@ -24417,16 +15052,21 @@ Dataman storage backend + If the board supports persistent storage (i.e., the KConfig variable DATAMAN_PERSISTENT_STORAGE is set), +the 'Default storage' backend uses a file on persistent storage. If not supported, this backend uses +non-persistent storage in RAM. true - Disabled - default (SD card) - RAM (not persistent) + Dataman disabled + Default storage + RAM storage Enable factory calibration mode - If enabled, future sensor calibrations will be stored to /fs/mtd_caldata. Note: this is only supported on boards with a separate calibration storage /fs/mtd_caldata. + If enabled, future sensor calibrations will be stored to /fs/mtd_caldata. +Note: this is only supported on boards with a separate calibration storage +/fs/mtd_caldata. Disabled All sensors @@ -24435,38 +15075,59 @@ Enable failure injection - If enabled allows MAVLink INJECT_FAILURE commands. WARNING: the failures can easily cause crashes and are to be used with caution! + If enabled allows MAVLink INJECT_FAILURE commands. +WARNING: the failures can easily cause crashes and are to be used with caution! Control if the vehicle has a barometer - Disable this if the board has no barometer, such as some of the Omnibus F4 SD variants. If disabled, the preflight checks will not check for the presence of a barometer. + Disable this if the board has no barometer, such as some of the Omnibus +F4 SD variants. +If disabled, the preflight checks will not check for the presence of a +barometer. true Control if the vehicle has a GPS - Disable this if the system has no GPS. If disabled, the sensors hub will not process sensor_gps, and GPS will not be available for the rest of the system. + Disable this if the system has no GPS. +If disabled, the sensors hub will not process sensor_gps, +and GPS will not be available for the rest of the system. true - Control if the vehicle has a magnetometer - Set this to 0 if the board has no magnetometer. If set to 0, the preflight checks will not check for the presence of a magnetometer, otherwise N sensors are required. + Control if and how many magnetometers are expected + 0: System has no magnetometer, preflight checks should pass without one. +1-N: Require the presence of N magnetometer sensors for check to pass. true Control if the vehicle has an airspeed sensor - Set this to 0 if the board has no airspeed sensor. If set to 0, the preflight checks will not check for the presence of an airspeed sensor. + Set this to 0 if the board has no airspeed sensor. +If set to 0, the preflight checks will not check for the presence of an +airspeed sensor. 0 1 Number of distance sensors to check being available - The preflight check will fail if fewer than this number of distance sensors with valid data is present. Disable the check with 0. + The preflight check will fail if fewer than this number of distance sensors with valid data is present. +Disable the check with 0. 0 4 + + Number of optical flow sensors required to be available + The preflight check will fail if fewer than this number of optical flow sensors with valid data are present. + 0 + 1 + Enable HITL/SIH mode on next boot - While enabled the system will boot in Hardware-In-The-Loop (HITL) or Simulation-In-Hardware (SIH) mode and not enable all sensors and checks. When disabled the same vehicle can be flown normally. Set to 'external HITL', if the system should perform as if it were a real vehicle (the only difference to a real system is then only the parameter value, which can be used for log analysis). + While enabled the system will boot in Hardware-In-The-Loop (HITL) +or Simulation-In-Hardware (SIH) mode and not enable all sensors and checks. +When disabled the same vehicle can be flown normally. +Set to 'external HITL', if the system should perform as if it were a real +vehicle (the only difference to a real system is then only the parameter +value, which can be used for log analysis). true external HITL @@ -24477,7 +15138,10 @@ Parameter version - This is used internally only: an airframe configuration might set an expected parameter version value via PARAM_DEFAULTS_VER. This is checked on bootup against SYS_PARAM_VER, and if they do not match, parameters are reset and reloaded from the airframe configuration. + This is used internally only: an airframe configuration might set an expected +parameter version value via PARAM_DEFAULTS_VER. This is checked on bootup +against SYS_PARAM_VER, and if they do not match, parameters are reset and +reloaded from the airframe configuration. 0 @@ -24489,51 +15153,6 @@ Enable stack checking - - - Blacksheep telemetry Enable - If true, the FMU will try to connect to Blacksheep telemetry on start up - true - - - Serial Configuration for FrSky Telemetry - Configure on which serial port to run FrSky Telemetry. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for HoTT Telemetry - Configure on which serial port to run HoTT Telemetry. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - TEST_1 @@ -25304,307 +15923,60 @@ true - - - Sagetech External Configuration Mode - Disables auto-configuration mode enabling MXS config through external software. - true - - - Sagetech MXS mode configuration - This parameter defines the operating mode of the MXS - 0 - 3 - false - - Off - On - Standby - Alt - - - - Serial Configuration for Sagetech MXS Serial Port - Configure on which serial port to run Sagetech MXS Serial Port. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Sagetech MXS Participant Configuration - The MXS communication port to receive Target data from - 0 - 2 - false - - Auto - COM0 - COM1 - - - - - - UAVCAN CAN bus bitrate - 20000 - 1000000 - - - Enable MovingBaselineData publication - true - - - Enable MovingBaselineData subscription - 1 - true - - - Enable RTCM subscription - true - - - CAN built-in bus termination - 1 - - - Simulator Gazebo bridge enable - true - - - UAVCAN CAN bus bitrate - 20000 - 1000000 - bit/s - true - - - UAVCAN fuel tank fuel type - This parameter defines the type of fuel used in the vehicle's fuel tank. 0: Unknown 1: Liquid (e.g., gasoline, diesel) 2: Gas (e.g., hydrogen, methane, propane) - 0 - 2 - true - - Unknown - Liquid - Gas - - - - UAVCAN fuel tank maximum capacity - This parameter defines the maximum fuel capacity of the vehicle's fuel tank. - 0.0 - 100000.0 - liters - 1 - 0.1 - true - - - UAVCAN mode - 0 - UAVCAN disabled. 1 - Enables support for UAVCAN sensors without dynamic node ID allocation and firmware update. 2 - Enables support for UAVCAN sensors with dynamic node ID allocation and firmware update. 3 - Enables support for UAVCAN sensors and actuators with dynamic node ID allocation and firmware update. Also sets the motor control outputs to UAVCAN. - 0 - 3 - true - - Disabled - Sensors Manual Config - Sensors Automatic Config - Sensors and Actuators (ESCs) Automatic Config - - - - UAVCAN ANTI_COLLISION light operating mode - This parameter defines the minimum condition under which the system will command the ANTI_COLLISION lights on 0 - Always off 1 - When autopilot is armed 2 - When autopilot is prearmed 3 - Always on - 0 - 3 - true - - Always off - When autopilot is armed - When autopilot is prearmed - Always on - - - - UAVCAN LIGHT_ID_LANDING light operating mode - This parameter defines the minimum condition under which the system will command the LIGHT_ID_LANDING lights on 0 - Always off 1 - When autopilot is armed 2 - When autopilot is prearmed 3 - Always on - 0 - 3 - true - - Always off - When autopilot is armed - When autopilot is prearmed - Always on - - - - UAVCAN RIGHT_OF_WAY light operating mode - This parameter defines the minimum condition under which the system will command the RIGHT_OF_WAY lights on 0 - Always off 1 - When autopilot is armed 2 - When autopilot is prearmed 3 - Always on - 0 - 3 - true - - Always off - When autopilot is armed - When autopilot is prearmed - Always on - - - - UAVCAN STROBE light operating mode - This parameter defines the minimum condition under which the system will command the STROBE lights on 0 - Always off 1 - When autopilot is armed 2 - When autopilot is prearmed 3 - Always on + + + Height rc-button down 0 - 3 - true - - Always off - When autopilot is armed - When autopilot is prearmed - Always on - - - - UAVCAN Node ID - Read the specs at http://uavcan.org to learn more about Node ID. - 1 - 125 - true - - - publish Arming Status stream - Enable UAVCAN Arming Status stream publication uavcan::equipment::safety::ArmingStatus - true - - - publish moving baseline data RTCM stream - Enable UAVCAN RTCM stream publication ardupilot::gnss::MovingBaselineData - true - - - publish RTCM stream - Enable UAVCAN RTCM stream publication uavcan::equipment::gnss::RTCMStream - true - - - UAVCAN rangefinder maximum range - This parameter defines the maximum valid range for a rangefinder connected via UAVCAN. - m - - - UAVCAN rangefinder minimum range - This parameter defines the minimum valid range for a rangefinder connected via UAVCAN. - m - - - subscription airspeed - Enable UAVCAN airspeed subscriptions. uavcan::equipment::air_data::IndicatedAirspeed uavcan::equipment::air_data::TrueAirspeed uavcan::equipment::air_data::StaticTemperature - true - - - subscription barometer - Enable UAVCAN barometer subscription. uavcan::equipment::air_data::StaticPressure uavcan::equipment::air_data::StaticTemperature - true + 16 - - subscription battery - Enable UAVCAN battery subscription. uavcan::equipment::power::BatteryInfo ardupilot::equipment::power::BatteryInfoAux 0 - Disable 1 - Use raw data. Recommended for Smart battery 2 - Filter the data with internal battery library + + Height rc-button up 0 - 2 - true - - Disable - Raw data - Filter data - - - - subscription button - Enable UAVCAN button subscription. ardupilot::indication::Button - true - - - subscription differential pressure - Enable UAVCAN differential pressure subscription. uavcan::equipment::air_data::RawAirData - true - - - subscription flow - Enable UAVCAN optical flow subscription. - true - - - subscription fuel tank - Enable UAVCAN fuel tank status subscription. - true - - - subscription GPS - Enable UAVCAN GPS subscriptions. uavcan::equipment::gnss::Fix uavcan::equipment::gnss::Fix2 uavcan::equipment::gnss::Auxiliary - true - - - subscription GPS Relative - Enable UAVCAN GPS Relative subscription. ardupilot::gnss::RelPosHeading - true + 16 - - subscription hygrometer - Enable UAVCAN hygrometer subscriptions. dronecan::sensors::hygrometer::Hygrometer - true + + Height differential gain + 2 - - subscription ICE - Enable UAVCAN internal combustion engine (ICE) subscription. uavcan::equipment::ice::reciprocating::Status - true + + Height integrational gain + 2 - - subscription IMU - Enable UAVCAN IMU subscription. uavcan::equipment::ahrs::RawIMU - true + + sum speed of error for integrational gain + 2 - - subscription magnetometer - Enable UAVCAN mag subscription. uavcan::equipment::ahrs::MagneticFieldStrength uavcan::equipment::ahrs::MagneticFieldStrength2 - true + + maximum Height distance controlled by manual input. Diff between actual and desired Height cant be higher than that + 2 - - subscription range finder - Enable UAVCAN range finder subscription. uavcan::equipment::range_sensor::Measurement - true + + Height proportional gain + 2 - - - - Direct pitch input + + Height change strength from manual input + 2 - - Direct roll input + + Pitch gain for manual inputs in manual control mode + 0.0 + 2 - - Direct thrust input + + Roll gain for manual inputs in manual control mode + 0.0 + 2 - - Direct yaw input + + Throttle gain for manual inputs in manual control mode + 0.0 + 2 - - Select Input Mode - - use Attitude Setpoints - Direct Feedthrough - + + Yaw gain for manual inputs in manual control mode + 0.0 + 2 Pitch differential gain @@ -25614,6 +15986,26 @@ Pitch proportional gain 2 + + Pitch gain for manual inputs in rate control mode + 0.0 + 2 + + + Roll gain for manual inputs in rate control mode + 0.0 + 2 + + + Throttle gain for manual inputs in rate control mode + 0.0 + 2 + + + Yaw gain for manual inputs in rate control mode + 0.0 + 2 + Roll differential gain 2 @@ -25622,6 +16014,46 @@ Roll proportional gain 2 + + Pitch gain for manual inputs in attitude control mode + 0.0 + 2 + + + Roll gain for manual inputs in attitude control mode + 0.0 + 2 + + + Throttle gain for manual inputs in attitude control mode + 0.0 + 2 + + + Yaw gain for manual inputs in attitude control mode + 0.0 + 2 + + + Maximum time (in seconds) before resetting setpoint + + + Stick mode selector (0=Heave/sway control, roll/pitch leveled; 1=Pitch/roll control) + 0 + 1 + + + UUV Thrust setpoint Saturation + 0.0 + 1.0 + 2 + + + UUV Torque setpoint Saturation + 0.0 + 1.0 + 2 + Yaw differential gain 2 @@ -25650,149 +16082,78 @@ Gain of P controller Z - + + Gain for position control velocity setpoint update + + Stabilization mode(1) or Position Control(0) - Position Control - Stabilization Mode + Moves position setpoint in world frame + Moves position setpoint in body frame - - - - UWB sensor X offset in body frame - UWB sensor positioning in relation to Drone in NED. X offset. A Positive offset results in a Position o - m - 2 - 0.01 - - - UWB sensor Y offset in body frame - UWB sensor positioning in relation to Drone in NED. Y offset. - m - 2 - 0.01 - - - UWB sensor Z offset in body frame - UWB sensor positioning in relation to Drone in NED. Z offset. - m - 2 - 0.01 + + Deadband for changing position setpoint - - Serial Configuration for Ultrawideband position sensor driver - Configure on which serial port to run Ultrawideband position sensor driver. - true + + Stabilization mode(1) or Position Control(0) - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - UWB sensor orientation - The orientation of the sensor relative to the forward direction of the body frame. Look up table in src/lib/conversion/rotation.h Default position is the antannaes downward facing, UWB board parallel with body frame. - - ROTATION_NONE - ROTATION_YAW_45 - ROTATION_YAW_90 - ROTATION_YAW_135 - ROTATION_YAW_180 - ROTATION_YAW_225 - ROTATION_YAW_270 - ROTATION_YAW_315 - ROTATION_ROLL_180 - ROTATION_ROLL_180_YAW_45 - ROTATION_ROLL_180_YAW_90 - ROTATION_ROLL_180_YAW_135 - ROTATION_PITCH_180 - ROTATION_ROLL_180_YAW_225 - ROTATION_ROLL_180_YAW_270 - ROTATION_ROLL_180_YAW_315 - ROTATION_ROLL_90 - ROTATION_ROLL_90_YAW_45 - ROTATION_ROLL_90_YAW_90 - ROTATION_ROLL_90_YAW_135 - ROTATION_ROLL_270 - ROTATION_ROLL_270_YAW_45 - ROTATION_ROLL_270_YAW_90 - ROTATION_ROLL_270_YAW_135 - ROTATION_PITCH_90 - ROTATION_PITCH_270 - ROTATION_PITCH_180_YAW_90 - ROTATION_PITCH_180_YAW_270 - ROTATION_ROLL_90_PITCH_90 - ROTATION_ROLL_180_PITCH_90 - ROTATION_ROLL_270_PITCH_90 - ROTATION_ROLL_90_PITCH_180 - ROTATION_ROLL_270_PITCH_180 - ROTATION_ROLL_90_PITCH_270 - ROTATION_ROLL_180_PITCH_270 - ROTATION_ROLL_270_PITCH_270 - ROTATION_ROLL_90_PITCH_180_YAW_90 - ROTATION_ROLL_90_YAW_270 - ROTATION_ROLL_90_PITCH_68_YAW_293 - ROTATION_PITCH_315 - ROTATION_ROLL_90_PITCH_315 + Tracks previous attitude setpoint + Tracks horizontal attitude (allows yaw change) uXRCE-DDS Agent IP address - If ethernet is enabled and is the selected configuration for uXRCE-DDS, the selected Agent IP address will be set and used. Decimal dot notation is not supported. IP address must be provided in int32 format. For example, 192.168.1.2 is mapped to -1062731518; 127.0.0.1 is mapped to 2130706433. + If ethernet is enabled and is the selected configuration for uXRCE-DDS, +the selected Agent IP address will be set and used. +Decimal dot notation is not supported. IP address must be provided +in int32 format. For example, 192.168.1.2 is mapped to -1062731518; +127.0.0.1 is mapped to 2130706433. True - - Serial Configuration for UXRCE-DDS Client - Configure on which serial port to run UXRCE-DDS Client. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - Ethernet - - uXRCE-DDS domain ID uXRCE-DDS domain ID True + + Enable serial flow control for UXRCE interface + This is used to enable flow control for the serial uXRCE instance. +Used for reliable high bandwidth communication. + True + uXRCE-DDS session key - uXRCE-DDS key, must be different from zero. In a single agent - multi client configuration, each client must have a unique session key. + uXRCE-DDS key, must be different from zero. +In a single agent - multi client configuration, each client +must have a unique session key. + True + + + Define an index-based message namespace + Defines an index-based namespace for DDS messages, e.g, uav_0, uav_1, up to uav_9999 +A value less than zero leaves the namespace empty + -1 + 9999 True uXRCE-DDS UDP port - If ethernet is enabled and is the selected configuration for uXRCE-DDS, the selected UDP port will be set and used. + If ethernet is enabled and is the selected configuration for uXRCE-DDS, +the selected UDP port will be set and used. 0 65535 True uXRCE-DDS participant configuration - Set the participant configuration on the Agent's system. 0: Use the default configuration. 1: Restrict messages to localhost (use in combination with ROS_LOCALHOST_ONLY=1). 2: Use a custom participant with the profile name "px4_participant". + Set the participant configuration on the Agent's system. +0: Use the default configuration. +1: Restrict messages to localhost +(use in combination with ROS_LOCALHOST_ONLY=1). +2: Use a custom participant with the profile name "px4_participant". 0 2 True @@ -25802,6 +16163,13 @@ Custom participant + + RX rate timeout configuration + Specifies after how many seconds without receiving data the DDS connection is reestablished. +A value less than one disables the RX rate timeout. + s + True + Enable uXRCE-DDS system clock synchronization When enabled along with UXRCE_DDS_SYNCT, uxrce_dds_client will set the system clock using the agents UTC timestamp. @@ -25812,171 +16180,12 @@ When enabled, uxrce_dds_client will synchronize the timestamps of the incoming and outgoing messages measuring the offset between the Agent OS time and the PX4 time. True - - - - UART ESC baud rate - Default rate is 250Kbps, which is used in off-the-shelf MoadalAI ESC products. - bit/s - - - UART ESC configuration - Selects what type of UART ESC, if any, is being used. - 0 - 1 - true - - - Disabled - - VOXL ESC - - - - UART ESC Mode - Selects what type of mode is enabled, if any - 0 - 2 - true - - - None - - Turtle Mode enabled via AUX1 - - Turtle Mode enabled via AUX2 - - UART Passthrough Mode - - - - UART ESC Enable publishing of battery status - Only applicable to ESCs that report total battery voltage and current - 0 - 1 - true - - - Disabled - - Enabled - - - - UART ESC RPM Max - Maximum RPM for ESC - rpm - - - UART ESC RPM Min - Minimum RPM for ESC - rpm - - - UART ESC ID 1 Spin Direction Flag - - - Default - - Reverse - - - - UART ESC ID 2 Spin Direction Flag - - - Default - - Reverse - - - - UART ESC ID 3 Spin Direction Flag - - - Default - - Reverse - - - - UART ESC ID 4 Spin Direction Flag - - - Default - - Reverse - - - - UART ESC Turtle Mode Cosphi - 0.000 - 1.000 - 10 - 0.001 - - - UART ESC Turtle Mode Crash Flip Motor Deadband - 0 - 100 - 10 - 1 - - - UART ESC Turtle Mode Crash Flip Motor expo - 0 - 100 - 10 - 1 - - - UART ESC Turtle Mode Crash Flip Motor STICK_MINF - 0.0 - 100.0 - 10 - 1.0 - - - UART ESC Over-Temperature Threshold (Degrees C) - Only applicable to ESCs that report temperature - 0 - 200 - true - - - Disabled - - - - UART ESC Turtle Mode Crash Flip Motor Percent - 1 - 100 - 10 - 1 - - - UART ESC Temperature Warning Threshold (Degrees C) - Only applicable to ESCs that report temperature - 0 - 200 - true - - - Disabled - - - - UART ESC verbose logging - 0 - 1 - true - - - Disabled - - Enabled - - - - - - UART M0065 baud rate - Default rate is 921600, which is used for communicating with M0065. - bit/s - - - M0065 PWM Max - Maximum duration (microseconds) for M0065 PWM - 0 - 2000 - us - - - M0065 PWM Min - Minimum duration (microseconds) for M0065 PWM - 0 - 2000 - us + + TX rate timeout configuration + Specifies after how many seconds without sending data the DDS connection is reestablished. +A value less than one disables the TX rate timeout. + s + True @@ -26008,7 +16217,7 @@ 0.1 - Backtransition deceleration setpoint to pitch I gain + Backtransition deceleration setpoint to tilt I gain 0 0.3 rad s/m @@ -26017,7 +16226,8 @@ Approximate deceleration during back transition - Used to calculate back transition distance in an auto mode. For standard vtol and tiltrotors a controller is used to track this value during the transition. + Used to calculate back transition distance in an auto mode. +For standard vtol and tiltrotors a controller is used to track this value during the transition. 0.5 10 m/s^2 @@ -26048,28 +16258,36 @@ Use fixed-wing actuation in hover to accelerate forward - This feature can be used to avoid the plane having to pitch nose down in order to move forward. Prevents large, negative lift from pitching nose down into wind. Fixed-wing forward actuators refers to puller/pusher (standard VTOL), or forward-tilt (tiltrotor VTOL). Only active if demanded down pitch is below VT_PITCH_MIN. Use VT_FWD_THRUST_SC to tune it. Descend mode is treated as Landing too. Only active (if enabled) in Altitude, Position and Auto modes, not in Stabilized. + Prevents downforce from pitching the body down when facing wind. +Uses puller/pusher (standard VTOL), or forward-tilt (tiltrotor VTOL) to accelerate forward instead. +Only active if demanded pitch is below VT_PITCH_MIN. +Use VT_FWD_THRUST_SC to tune it. +Descend mode is treated as Landing too. +Only active (if enabled) in height-rate controlled modes. Disabled Enabled (except LANDING) - Enabled if distance to ground above MPC_LAND_ALT1 - Enabled if distance to ground above MPC_LAND_ALT2 + Enabled if above MPC_LAND_ALT1 + Enabled if above MPC_LAND_ALT2 Enabled constantly - Enabled if distance to ground above MPC_LAND_ALT1 (except LANDING) - Enabled if distance to ground above MPC_LAND_ALT2 (except LANDING) + Enabled if above MPC_LAND_ALT1 (except LANDING) + Enabled if above MPC_LAND_ALT2 (except LANDING) - Fixed-wing actuation thrust scale for hover forward flight - Scale applied to the demanded down-pitch to get the fixed-wing forward actuation in hover mode. Enabled via VT_FWD_THRUST_EN. + Fixed-wing actuation thrust scale in hover + Scale applied to the demanded pitch (below VT_PITCH_MIN) to get the fixed-wing forward actuation in hover mode. +Enabled via VT_FWD_THRUST_EN. 0.0 - 2.0 + 5.0 2 0.01 Differential thrust in forwards flight - Enable differential thrust seperately for roll, pitch, yaw in forward (fixed-wing) mode. The effectiveness of differential thrust around the corresponding axis can be tuned by setting VT_FW_DIFTHR_S_R / VT_FW_DIFTHR_S_P / VT_FW_DIFTHR_S_Y. + Enable differential thrust seperately for roll, pitch, yaw in forward (fixed-wing) mode. +The effectiveness of differential thrust around the corresponding axis can be +tuned by setting VT_FW_DIFTHR_S_R / VT_FW_DIFTHR_S_P / VT_FW_DIFTHR_S_Y. 0 7 @@ -26104,7 +16322,9 @@ Quad-chute altitude - Minimum altitude for fixed-wing flight. When the vehicle is in fixed-wing mode and the altitude drops below this altitude (relative altitude above local origin), it will instantly switch back to MC mode and execute behavior defined in COM_QC_ACT. + Minimum altitude for fixed-wing flight. When the vehicle is in fixed-wing mode +and the altitude drops below this altitude (relative altitude above local origin), +it will instantly switch back to MC mode and execute behavior defined in COM_QC_ACT. 0.0 200.0 m @@ -26113,21 +16333,27 @@ Quad-chute maximum height - Maximum height above the ground (if available, otherwise above Home if available, otherwise above the local origin) where triggering a quad-chute is possible. At high altitudes there is a big risk to deplete the battery and therefore crash if quad-chuting there. + Maximum height above the ground (if available, otherwise above +Home if available, otherwise above the local origin) where triggering a quad-chute is possible. +At high altitudes there is a big risk to deplete the battery and therefore crash if quad-chuting there. 0 m 1 Quad-chute max pitch threshold - Absolute pitch threshold for quad-chute triggering in FW mode. Above this the vehicle will transition back to MC mode and execute behavior defined in COM_QC_ACT. Set to 0 do disable this threshold. + Absolute pitch threshold for quad-chute triggering in FW mode. +Above this the vehicle will transition back to MC mode and execute behavior defined in COM_QC_ACT. +Set to 0 do disable this threshold. 0 180 deg Quad-chute max roll threshold - Absolute roll threshold for quad-chute triggering in FW mode. Above this the vehicle will transition back to MC mode and execute behavior defined in COM_QC_ACT. Set to 0 do disable this threshold. + Absolute roll threshold for quad-chute triggering in FW mode. +Above this the vehicle will transition back to MC mode and execute behavior defined in COM_QC_ACT. +Set to 0 do disable this threshold. 0 180 deg @@ -26150,25 +16376,29 @@ Airspeed-less front transition time (open loop) - The duration of the front transition when there is no airspeed feedback available. + The duration of the front transition when there is no airspeed feedback available. +When airspeed is used, transition timeout is declared if airspeed does not +reach VT_ARSP_BLEND after this time. 1.0 30.0 s 1 0.5 - + Minimum pitch angle during hover landing - Overrides VT_PITCH_MIN when the vehicle is in LAND mode (hovering). During landing it can be beneficial to reduce the pitch angle to reduce the generated lift in head wind. + Overrides VT_PITCH_MIN when the vehicle is in LAND mode (hovering). +During landing it can be beneficial to reduce the pitch angle to reduce the generated lift in head wind. -10.0 45.0 deg 1 0.1 - + Minimum pitch angle during hover - Any pitch setpoint below this value is translated to a forward force by the fixed-wing forward actuation if VT_FW_TRHUST_EN is set to 1. + Any pitch setpoint below this value is translated to a forward force by the fixed-wing forward actuation if +VT_FWD_TRHUST_EN is set. -10.0 45.0 deg @@ -26177,7 +16407,9 @@ Pusher throttle ramp up slew rate - Defines the slew rate of the puller/pusher throttle during transitions. Zero will deactivate the slew rate limiting and thus produce an instant throttle rise to the transition throttle VT_F_TRANS_THR. + Defines the slew rate of the puller/pusher throttle during transitions. +Zero will deactivate the slew rate limiting and thus produce an instant throttle +rise to the transition throttle VT_F_TRANS_THR. 0 1/s 2 @@ -26185,7 +16417,11 @@ Quad-chute uncommanded descent threshold - Altitude error threshold for quad-chute triggering during fixed-wing flight. The check is only active if altitude is controlled and the vehicle is below the current altitude reference. The altitude error is relative to the highest altitude the vehicle has achieved since it has flown below the current altitude reference. Set to 0 do disable. + Altitude error threshold for quad-chute triggering during fixed-wing flight. +The check is only active if altitude is controlled and the vehicle is below the current altitude reference. +The altitude error is relative to the highest altitude the vehicle has achieved since it has flown below the current +altitude reference. +Set to 0 do disable. 0.0 200.0 m @@ -26194,7 +16430,12 @@ Quad-chute transition altitude loss threshold - Altitude loss threshold for quad-chute triggering during VTOL transition to fixed-wing flight in altitude-controlled flight modes. Active until 5s after completing transition to fixed-wing. If the current altitude is more than this value below the altitude at the beginning of the transition, it will instantly switch back to MC mode and execute behavior defined in COM_QC_ACT. Set to 0 do disable this threshold. + Altitude loss threshold for quad-chute triggering during VTOL transition to fixed-wing flight +in altitude-controlled flight modes. +Active until 5s after completing transition to fixed-wing. +If the current altitude is more than this value below the altitude at the beginning of the +transition, it will instantly switch back to MC mode and execute behavior defined in COM_QC_ACT. +Set to 0 do disable this threshold. 0 50 m @@ -26250,7 +16491,7 @@ Front transition timeout - Time in seconds after which transition will be cancelled. Disabled if set to 0. + Time in seconds after which transition will be cancelled. 0.1 30.00 s @@ -26289,290 +16530,4 @@ 1 - - - Serial Configuration for Vertiq IO - Configure on which serial port to run Vertiq IO. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - The triggered behavior on PX4 arm - The behavior triggered when the flight controller arms. You have the option to use your motors' arming behaviors, or to force all of your motors to arm - - Use Motor Arm Behavior - Send Explicit Arm Command - - - - The IQUART driver's baud rate - The baud rate (in bits per second) used by the serial port connected with IQUART communication - True - - - Module Param - The module's control mechanism - PWM Mode: Commands a fraction of battery voltage. This changes as the battery voltage changes. This is the least safe mode because the upper throttle limit is determined by the battery voltage. Voltage Mode: Commands a voltage. The motor will behave the same way throughout the life of a battery, assuming the commanded voltage is less than the battery voltage. You must set the MAX_VOLTS parameter. Velocity Mode: Closed-loop, commands a velocity. The controller will adjust the applied voltage so that the motor spins at the commanded velocity. This mode has faster reaction times. Only use this if you know the properties of your propeller. You must set the MAX_VELOCITY parameter. - - PWM - Voltage - Velocity - - - - The triggered behavior sent to the motors on PX4 disarm - The behavior triggered when the flight controller disarms. You have the option to trigger your motors' disarm behaviors, set all motors to coast, or set a predefined throttle setpoint - - Send Explicit Disarm - Coast Motors - Set Predefined Velocity Setpoint - - - - Velocity sent when DISARM_TRIGGER is Set Predefined Velocity Setpoint - This is the velocity that will be sent to all motors when PX4 is disarmed and DISARM_TRIGGER is Set Predefined Velocity Setpoint - 0 - 100 - - - Module Param - If the flight controller uses 2D or 3D communication - The FC and the ESC must agree upon the meaning of the signal coming out of the ESC. When FCs are in 3D mode they re-map negative signals. This parameter keeps the FC and ESC in agreement. - - 2D - 3D - - - - Module Param - Maximum velocity when CONTROL_MODE is set to Velocity - Only relevant in Velocity Mode. This is the velocity the controller will command at full throttle. - - - Module Param - Maximum voltage when CONTROL_MODE is set to Voltage - Only relevant in Voltage Mode. This is the voltage the controller will command at full throttle. - - - Module Param - The direction that the module should spin - Set the targeted motor's spinning direction (clockwise vs. counter clockwise) and flight mode (2D non-reversible vs. 3D reversible) - - Unconfigured - 3D Counter Clockwise - 3D Clockwise - 2D Counter Clockwise - 2D Clockwise - - - - The number of Vertiq IFCI parameters to use - The total number of IFCI control variables being used across all connected modules - 0 - 16 - True - - - Module Param - Max pulsing voltage limit when in Voltage Limit Mode - This sets the max pulsing voltage limit when in Voltage Limit Mode. - - - Module Param - 0 = Supply Voltage Mode, 1 = Voltage Limit Mode - Supply Voltage Mode means that the maximum voltage applied to pulsing is the supplied voltage. Voltage Limit Mode indicates that PULSE_VOLT_LIM is the maximum allowed voltage to apply towards pulsing. - - Supply Voltage Mode - Voltage Limit Mode - - - - Reinitialize the target module's values into the PX4 parameters - Setting this value to true will reinitialize PX4's IQUART connected parameters to the value stored on the currently targeted motor. This is especially useful if your flight controller powered on before your connected modules - - - Module IDs [0, 31] that you would like to request telemetry from - The module IDs [0, 31] that should be asked for telemetry. The data received from these IDs will be published via the esc_status topic. - 0 - 4294967295 - True - - Module ID 0 - Module ID 1 - Module ID 2 - Module ID 3 - Module ID 4 - Module ID 5 - Module ID 6 - Module ID 7 - Module ID 8 - Module ID 9 - Module ID 10 - Module ID 11 - Module ID 12 - Module ID 13 - Module ID 14 - Module ID 15 - Module ID 16 - Module ID 17 - Module ID 18 - Module ID 19 - Module ID 20 - Module ID 21 - Module ID 22 - Module ID 23 - Module ID 24 - Module ID 25 - Module ID 26 - Module ID 27 - Module ID 28 - Module ID 29 - Module ID 30 - Module ID 31 - - - - Module IDs [32, 62] that you would like to request telemetry from - The module IDs [32, 62] that should be asked for telemetry. The data received from these IDs will be published via the esc_status topic. - 0 - 2147483647 - True - - Module ID 32 - Module ID 33 - Module ID 34 - Module ID 35 - Module ID 36 - Module ID 37 - Module ID 38 - Module ID 39 - Module ID 40 - Module ID 41 - Module ID 42 - Module ID 43 - Module ID 44 - Module ID 45 - Module ID 46 - Module ID 47 - Module ID 48 - Module ID 49 - Module ID 50 - Module ID 51 - Module ID 52 - Module ID 53 - Module ID 54 - Module ID 55 - Module ID 56 - Module ID 57 - Module ID 58 - Module ID 59 - Module ID 60 - Module ID 61 - Module ID 62 - - - - Module Param - The module's Throttle Control Value Index - This represents the Control Value Index where the targeted module will look for throttle commands - 0 - 255 - - - Module Param - Offsets pulse angle to allow for mechanical properties - This offsets where the pulse starts around the motor to allow for propeller mechanical properties. - - - The Module ID of the module you would like to communicate with - This is the value used as the target module ID of all configuration parameters (not operational parameters). The Vertiq module with the module ID matching this value will react to all get and set requests from PX4. Any Vertiq client made with dynamic object IDs should use this value to instantiate itself. - - - Module Param - The minimum velocity required to allow pulsing - This is the velocity at which pulsing is allowed. Any velocity between VELOCITY_CUTOFF and -VELOCITY_CUTOFF will not pulse. - - - Module Param - CVI for the X rectangular coordinate - This represents the Control Value Index where the targeted module will look for the X rectangular coordinate. - 0 - 255 - - - Module Param - CVI for the Y rectangular coordinate - This represents the Control Value Index where the targeted module will look for the Y rectangular coordinate. - 0 - 255 - - - Module Param - The encoder angle at which theta is zero - The encoder angle at which theta is zero. Adjust this number to change the location of 0 phase when pulsing. - - - - - Zenoh Enable - Zenoh - True - - - - - Accel filter settings - true - - 13 Hz - 30 Hz - 68 Hz - 235 Hz - 280 Hz - 370 Hz - No filter - - - - Gyro and Accel decimation settings - true - - None - 5900 Hz - 2950 Hz - 1475 Hz - 738 Hz - - - - Gyro filter settings - true - - 13 Hz - 30 Hz - 68 Hz - 235 Hz - 280 Hz - 370 Hz - No filter - - - - Lightware SF1xx/SF20/LW20 Operation Mode - 0 - 2 - - Disabled - Enabled - Enabled in VTOL MC mode, listen to request from system in FW mode - - - - Skip the controller - - use the module's controller - skip the controller and feedthrough the setpoints - - - From 504c2ec1e8e95b64a965f0c73b81ae68a0abeffe Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Mon, 16 Mar 2026 10:59:35 -0700 Subject: [PATCH 109/119] PX4: Rename RC Loss to RC/Joystick Loss in safety UI PX4 now centralizes handling of Joystick and RC controllers, so RC loss applies to all configured manual controllers. Update labels accordingly. --- src/AutoPilotPlugins/PX4/SafetyComponent.qml | 4 ++-- src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AutoPilotPlugins/PX4/SafetyComponent.qml b/src/AutoPilotPlugins/PX4/SafetyComponent.qml index 8475e7f385a6..65956b0fd967 100644 --- a/src/AutoPilotPlugins/PX4/SafetyComponent.qml +++ b/src/AutoPilotPlugins/PX4/SafetyComponent.qml @@ -213,7 +213,7 @@ SetupPage { } QGCLabel { - text: qsTr("RC Loss Failsafe Trigger") + text: qsTr("RC/Joystick Loss Failsafe Trigger") } Rectangle { @@ -253,7 +253,7 @@ SetupPage { } QGCLabel { - text: qsTr("RC Loss Timeout:") + text: qsTr("RC/Joystick Loss Timeout:") Layout.fillWidth: true } FactTextField { diff --git a/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml b/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml index 5a94367ff3ca..1284495ef77a 100644 --- a/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml @@ -33,12 +33,12 @@ Item { } VehicleSummaryRow { - labelText: qsTr("RC Loss Failsafe") + labelText: qsTr("RC/Joystick Loss Failsafe") valueText: rcLossAction ? rcLossAction.enumStringValue : "" } VehicleSummaryRow { - labelText: qsTr("RC Loss Timeout") + labelText: qsTr("RC/Joystick Loss Timeout") valueText: commRCLossFact ? commRCLossFact.valueString + " " + commRCLossFact.units : "" } From 0b99d8b87285483691f21dc3270701a4e5dd8977 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Mon, 16 Mar 2026 10:43:09 -0700 Subject: [PATCH 110/119] Fix lupdate JSON path and add weekly schedule - Fix qgc-lupdate-json.py to use 'src' instead of '../src' so it works when called from the repo root via qgc-lupdate.sh - Add weekly cron schedule (Sunday 03:00 UTC) to lupdate.yml workflow --- .github/workflows/lupdate.yml | 2 ++ tools/translations/README.md | 4 ++-- tools/translations/qgc-lupdate-json.py | 12 ++++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lupdate.yml b/.github/workflows/lupdate.yml index 5def9be2ddd3..86ce62f3da96 100644 --- a/.github/workflows/lupdate.yml +++ b/.github/workflows/lupdate.yml @@ -1,6 +1,8 @@ name: Update Translations on: + schedule: + - cron: '0 3 * * 0' # Every Sunday at 03:00 UTC workflow_dispatch: concurrency: diff --git a/tools/translations/README.md b/tools/translations/README.md index b079356ad469..f21f5c0215c7 100644 --- a/tools/translations/README.md +++ b/tools/translations/README.md @@ -10,9 +10,9 @@ Crowdin is configured to automatically sychronize the qgc.ts file once a day. So Add the new language from the CrowdIn settings as the first step. -### Periodically update the base transation files during the release cycle +### Periodically update the base translation files during the release cycle -You do this by running the `source tools/translations/qgc-lupdate.sh` script to update the translations files for both Qt and Json. Crowdin will automatically pull these up and submit a pull request back when new translations are available. +The `lupdate.yml` workflow runs automatically every Sunday to regenerate the `.ts` source files and open a PR with any changes. You can also trigger it manually from the Actions tab or run it locally with `./tools/translations/qgc-lupdate.sh` from the repository root. Crowdin will automatically pull these up and submit a pull request back when new translations are available. ## C++ and Qml code strings diff --git a/tools/translations/qgc-lupdate-json.py b/tools/translations/qgc-lupdate-json.py index e900b2972a60..27fa5cdd7583 100755 --- a/tools/translations/qgc-lupdate-json.py +++ b/tools/translations/qgc-lupdate-json.py @@ -108,7 +108,7 @@ def walkDirectoryTreeForJsonFiles(dir, multiFileLocArray): walkDirectoryTreeForJsonFiles(path, multiFileLocArray) -def writeJsonTSFile(multiFileLocArray): +def writeJsonTSFile(output_path, multiFileLocArray): ts_root = ET.Element("TS", version="2.1") for entry in multiFileLocArray: @@ -146,7 +146,7 @@ def writeJsonTSFile(multiFileLocArray): ET.SubElement(message, "source").text = sourceStr ET.SubElement(message, "translation", type="unfinished") - with open("translations/qgc-json.ts", "w", encoding="utf-8") as jsonTSFile: + with open(output_path, "w", encoding="utf-8") as jsonTSFile: jsonTSFile.write('\n') jsonTSFile.write("\n") jsonTSFile.write(ET.tostring(ts_root, encoding="unicode")) @@ -154,9 +154,13 @@ def writeJsonTSFile(multiFileLocArray): def main(): + # Resolve repo root from script location so CWD doesn't matter + script_dir = os.path.dirname(os.path.abspath(__file__)) + repo_root = os.path.normpath(os.path.join(script_dir, "..", "..")) + multiFileLocArray = [] - walkDirectoryTreeForJsonFiles("../src", multiFileLocArray) - writeJsonTSFile(multiFileLocArray) + walkDirectoryTreeForJsonFiles(os.path.join(repo_root, "src"), multiFileLocArray) + writeJsonTSFile(os.path.join(repo_root, "translations", "qgc-json.ts"), multiFileLocArray) if __name__ == "__main__": From b93f4c68fd930f33f6d94bcf4db0f044e184d4ee Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Tue, 17 Mar 2026 12:06:03 -0700 Subject: [PATCH 111/119] Refactor camera tracking and on-screen gimbal support Camera Tracking Interface: - Remove flawed TrackingStatus bitmask enum - Separate point/rect tracking into distinct properties - Rename signals to match property names (trackingImageIsActiveChanged, etc.) - Add supportsTrackingPoint/supportsTrackingRect Q_PROPERTYs - Split startTracking() overloads into startTrackingPoint()/startTrackingRect() - Add capability guards with qCCritical to both methods - setTrackingEnabled(false) now calls stopTracking() to notify vehicle - Cache hasTrackingImageStatus to prevent redundant signal emissions - Zero-initialize _trackingImageStatus struct FlyView Mouse Handling: - Extract camera tracking into OnScreenCameraTrackingController.qml - Refactor OnScreenGimbalController to method-based API - Clean 4-method API: mouseClicked/mouseDragStart/mouseDragPositionChanged/mouseDragEnd - Add click vs drag separation with 10px threshold - Remove hoverEnabled (unnecessary in full-screen mode) - Fix operator precedence bug in tracking status overlay - Fix mode guards bug in gimbal controller (was !value == 0) - Fix ROI rectangle resize on drag direction reversal - Declarative tracking status overlay (replaces imperative createObject/timer) - Gimbal rate timer unconditionally stopped in mouseDragEnd MockLink: - Add tracking simulation with figure-8 drift animation - Handle TRACK_POINT, TRACK_RECTANGLE, STOP_TRACKING commands - Handle SET_MESSAGE_INTERVAL for CAMERA_TRACKING_IMAGE_STATUS - Capability-gated commands (DENIED if flag not set) Gimbal Settings: - Rename ControlType to clickAndDrag boolean Relates-to: #13363 Relates-to: #13957 --- src/Camera/MavlinkCameraControlInterface.h | 39 ++-- src/Camera/QGCCameraManager.cc | 4 +- src/Camera/SimulatedCameraControl.h | 13 +- src/Camera/VehicleCameraControl.cc | 119 ++++++++---- src/Camera/VehicleCameraControl.h | 27 ++- src/Comms/MockLink/MockConfiguration.cc | 4 +- src/Comms/MockLink/MockConfiguration.h | 4 +- src/Comms/MockLink/MockLinkCamera.cc | 112 ++++++++++- src/Comms/MockLink/MockLinkCamera.h | 16 ++ src/FlyView/CMakeLists.txt | 1 + src/FlyView/FlyViewVideo.qml | 176 ++++-------------- .../OnScreenCameraTrackingController.qml | 150 +++++++++++++++ src/FlyView/OnScreenGimbalController.qml | 100 +++++----- src/Gimbal/GimbalController.cc | 26 +-- .../GimbalController.SettingsGroup.json | 18 +- src/Settings/GimbalControllerSettings.cc | 28 ++- src/Settings/GimbalControllerSettings.h | 10 +- src/UI/toolbar/GimbalIndicator.qml | 23 +-- 18 files changed, 549 insertions(+), 321 deletions(-) create mode 100644 src/FlyView/OnScreenCameraTrackingController.qml diff --git a/src/Camera/MavlinkCameraControlInterface.h b/src/Camera/MavlinkCameraControlInterface.h index 608c14a6e86a..d393d31d332f 100644 --- a/src/Camera/MavlinkCameraControlInterface.h +++ b/src/Camera/MavlinkCameraControlInterface.h @@ -44,6 +44,8 @@ class MavlinkCameraControlInterface : public FactGroup Q_PROPERTY(bool hasFocus READ hasFocus NOTIFY infoChanged) Q_PROPERTY(bool hasVideoStream READ hasVideoStream NOTIFY infoChanged) Q_PROPERTY(bool hasTracking READ hasTracking NOTIFY infoChanged) + Q_PROPERTY(bool supportsTrackingPoint READ supportsTrackingPoint NOTIFY infoChanged) + Q_PROPERTY(bool supportsTrackingRect READ supportsTrackingRect NOTIFY infoChanged) Q_PROPERTY(bool photosInVideoMode READ photosInVideoMode NOTIFY infoChanged) Q_PROPERTY(bool videoInPhotoMode READ videoInPhotoMode NOTIFY infoChanged) Q_PROPERTY(bool isBasic READ isBasic NOTIFY infoChanged) @@ -74,10 +76,14 @@ class MavlinkCameraControlInterface : public FactGroup Q_PROPERTY(QStringList streamLabels READ streamLabels NOTIFY streamLabelsChanged) Q_PROPERTY(ThermalViewMode thermalMode READ thermalMode WRITE setThermalMode NOTIFY thermalModeChanged) Q_PROPERTY(double thermalOpacity READ thermalOpacity WRITE setThermalOpacity NOTIFY thermalOpacityChanged) + + // Camera tracking properties Q_PROPERTY(bool trackingEnabled READ trackingEnabled WRITE setTrackingEnabled NOTIFY trackingEnabledChanged) - Q_PROPERTY(TrackingStatus trackingStatus READ trackingStatus CONSTANT) - Q_PROPERTY(bool trackingImageStatus READ trackingImageStatus NOTIFY trackingImageStatusChanged) - Q_PROPERTY(QRectF trackingImageRect READ trackingImageRect NOTIFY trackingImageStatusChanged) + Q_PROPERTY(bool trackingImageIsActive READ trackingImageIsActive NOTIFY trackingImageIsActiveChanged) + Q_PROPERTY(bool trackingImageIsPoint READ trackingImageIsPoint NOTIFY trackingImageIsPointChanged) + Q_PROPERTY(QRectF trackingImageRect READ trackingImageRect NOTIFY trackingImageRectChanged) + Q_PROPERTY(QPointF trackingImagePoint READ trackingImagePoint NOTIFY trackingImagePointChanged) + Q_PROPERTY(qreal trackingImageRadius READ trackingImageRadius NOTIFY trackingImageRadiusChanged) // These properties are used to determine what controls to show in the UI and are based on both the camera capabilities as well as the video manager status. // They are not necessarily directly related to the MAVLink camera capabilities. @@ -141,14 +147,6 @@ class MavlinkCameraControlInterface : public FactGroup }; Q_ENUM(ThermalViewMode) - enum TrackingStatus { - TRACKING_UNKNOWN = 0, - TRACKING_SUPPORTED = 1, - TRACKING_ENABLED = 2, - TRACKING_RECTANGLE = 4, - TRACKING_POINT = 8 - }; - Q_ENUM(TrackingStatus) Q_INVOKABLE virtual void setCameraModeVideo() = 0; Q_INVOKABLE virtual void setCameraModePhoto() = 0; @@ -165,8 +163,8 @@ class MavlinkCameraControlInterface : public FactGroup Q_INVOKABLE virtual void stopZoom() = 0; Q_INVOKABLE virtual void stopStream() = 0; Q_INVOKABLE virtual void resumeStream() = 0; - Q_INVOKABLE virtual void startTracking(QRectF rec) = 0; - Q_INVOKABLE virtual void startTracking(QPointF point, double radius) = 0; + Q_INVOKABLE virtual void startTrackingRect(QRectF rec) = 0; + Q_INVOKABLE virtual void startTrackingPoint(QPointF point, double radius) = 0; Q_INVOKABLE virtual void stopTracking() = 0; virtual int version() const = 0; @@ -182,6 +180,8 @@ class MavlinkCameraControlInterface : public FactGroup virtual bool hasZoom() const = 0; virtual bool hasFocus() const = 0; virtual bool hasTracking() const = 0; + virtual bool supportsTrackingPoint() const = 0; + virtual bool supportsTrackingRect() const = 0; virtual bool hasVideoStream() const = 0; virtual bool photosInVideoMode() const = 0; virtual bool videoInPhotoMode() const = 0; @@ -239,10 +239,11 @@ class MavlinkCameraControlInterface : public FactGroup virtual bool trackingEnabled() const = 0; virtual void setTrackingEnabled(bool set) = 0; - virtual TrackingStatus trackingStatus() const = 0; - - virtual bool trackingImageStatus() const = 0; + virtual bool trackingImageIsActive() const = 0; + virtual bool trackingImageIsPoint() const = 0; virtual QRectF trackingImageRect() const = 0; + virtual QPointF trackingImagePoint() const = 0; + virtual qreal trackingImageRadius() const = 0; virtual void factChanged(Fact *pFact) = 0; ///< Notify controller a parameter has changed virtual bool incomingParameter(Fact *pFact, QVariant &newValue) = 0; ///< Allow controller to modify or invalidate incoming parameter @@ -286,7 +287,11 @@ class MavlinkCameraControlInterface : public FactGroup void recordTimeChanged(); void streamLabelsChanged(); void trackingEnabledChanged(); - void trackingImageStatusChanged(); + void trackingImageIsActiveChanged(); + void trackingImageIsPointChanged(); + void trackingImageRectChanged(); + void trackingImagePointChanged(); + void trackingImageRadiusChanged(); void thermalModeChanged(); void thermalOpacityChanged(); void storageStatusChanged(); diff --git a/src/Camera/QGCCameraManager.cc b/src/Camera/QGCCameraManager.cc index d9d407ac7eb8..363d655dd180 100644 --- a/src/Camera/QGCCameraManager.cc +++ b/src/Camera/QGCCameraManager.cc @@ -796,8 +796,8 @@ void QGCCameraManager::_handleCameraFovStatus(const mavlink_message_t& message) } auto* settings = SettingsManager::instance()->gimbalControllerSettings(); - settings->CameraHFov()->setRawValue(fov.hfov); - settings->CameraVFov()->setRawValue(vfovDeg); + settings->cameraHFov()->setRawValue(fov.hfov); + settings->cameraVFov()->setRawValue(vfovDeg); } void QGCCameraManager::_setCurrentZoomLevel(int level) diff --git a/src/Camera/SimulatedCameraControl.h b/src/Camera/SimulatedCameraControl.h index f855af8547e4..0cf1aad785f3 100644 --- a/src/Camera/SimulatedCameraControl.h +++ b/src/Camera/SimulatedCameraControl.h @@ -35,8 +35,8 @@ class SimulatedCameraControl : public MavlinkCameraControlInterface void stopStream() override {} bool stopTakePhoto() override { return false;} void resumeStream() override {} - void startTracking(QRectF /*rec*/) override {} - void startTracking(QPointF /*point*/, double /*radius*/) override {} + void startTrackingRect(QRectF /*rec*/) override {} + void startTrackingPoint(QPointF /*point*/, double /*radius*/) override {} void stopTracking() override {} int version() const override { return 0; } @@ -52,6 +52,8 @@ class SimulatedCameraControl : public MavlinkCameraControlInterface bool hasZoom() const override { return false; } bool hasFocus() const override { return false; } bool hasTracking() const override { return false; } + bool supportsTrackingPoint() const override { return false; } + bool supportsTrackingRect() const override { return false; } bool hasVideoStream() const override; bool photosInVideoMode() const override { return true; } bool videoInPhotoMode() const override { return false; } @@ -105,10 +107,11 @@ class SimulatedCameraControl : public MavlinkCameraControlInterface bool trackingEnabled() const override { return false; } void setTrackingEnabled(bool /*set*/) override {} - TrackingStatus trackingStatus() const override { return TRACKING_UNKNOWN; } - - bool trackingImageStatus() const override { return false; } + bool trackingImageIsActive() const override { return false; } + bool trackingImageIsPoint() const override { return false; } QRectF trackingImageRect() const override { return QRectF(); } + QPointF trackingImagePoint() const override { return QPointF(); } + qreal trackingImageRadius() const override { return 0.0; } void factChanged(Fact* /*pFact*/) override {}; bool incomingParameter(Fact* /*pFact*/, QVariant& /*newValue*/) override { return false; } diff --git a/src/Camera/VehicleCameraControl.cc b/src/Camera/VehicleCameraControl.cc index 0a62c0a3baed..a6b37709c847 100644 --- a/src/Camera/VehicleCameraControl.cc +++ b/src/Camera/VehicleCameraControl.cc @@ -17,6 +17,8 @@ #include #include + +#include #include #include #include @@ -130,15 +132,9 @@ VehicleCameraControl::VehicleCameraControl(const mavlink_camera_information_t *i _videoRecordTimeUpdateTimer.setInterval(333); connect(&_videoRecordTimeUpdateTimer, &QTimer::timeout, this, &VehicleCameraControl::_recTimerHandler); - //-- Tracking - if(_mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_TRACKING_RECTANGLE) { - _trackingStatus = static_cast(_trackingStatus | TRACKING_RECTANGLE); - _trackingStatus = static_cast(_trackingStatus | TRACKING_SUPPORTED); - } - if(_mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_TRACKING_POINT) { - _trackingStatus = static_cast(_trackingStatus | TRACKING_POINT); - _trackingStatus = static_cast(_trackingStatus | TRACKING_SUPPORTED); - } + //-- Tracking capabilities + _hasTrackingRectCapability = _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_TRACKING_RECTANGLE; + _hasTrackingPointCapability = _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_TRACKING_POINT; connect(this, &VehicleCameraControl::dataReady, this, &VehicleCameraControl::_dataReady); @@ -1693,35 +1689,56 @@ void VehicleCameraControl::handleTrackingImageStatus(const mavlink_camera_tracki _trackingImageStatus = trackingImageStatus; - if (_trackingImageStatus.tracking_status == CAMERA_TRACKING_STATUS_FLAGS_IDLE || !trackingEnabled()) { - _trackingImageRect = {}; + const bool active = ((_trackingImageStatus.tracking_status & CAMERA_TRACKING_STATUS_FLAGS_ACTIVE) != 0) && trackingEnabled(); + const bool isPoint = active && (_trackingImageStatus.tracking_mode == CAMERA_TRACKING_MODE_POINT); + + if (!active) { qCDebug(CameraControlLog) << "Tracking off"; - } else { - if (_trackingImageStatus.tracking_mode == CAMERA_TRACKING_MODE_RECTANGLE) { - _trackingImageRect = QRectF(QPointF(_trackingImageStatus.rec_top_x, _trackingImageStatus.rec_top_y), - QPointF(_trackingImageStatus.rec_bottom_x, _trackingImageStatus.rec_bottom_y)); + _trackingImageRect = {}; + _trackingImagePoint = {}; + _trackingImageRadius = 0.0; + } else if (isPoint) { + const QPointF point(std::clamp(static_cast(_trackingImageStatus.point_x), 0.0, 1.0), + std::clamp(static_cast(_trackingImageStatus.point_y), 0.0, 1.0)); + qreal radius = static_cast(_trackingImageStatus.radius); + if (qIsNaN(radius) || radius <= 0) { + radius = 0.05; } else { - float r = _trackingImageStatus.radius; - if (qIsNaN(r) || r <= 0 ) { - r = 0.05f; - } - // Bottom is NAN so that we can draw perfect square using video aspect ratio - _trackingImageRect = QRectF(QPointF(_trackingImageStatus.point_x - r, _trackingImageStatus.point_y - r), - QPointF(_trackingImageStatus.point_x + r, NAN)); + radius = std::clamp(radius, 0.0, 1.0); + } + qCDebug(CameraControlLog) << "Tracking Point [" << point << "] radius:" << radius; + _trackingImageRect = {}; + if (_trackingImagePoint != point) { + _trackingImagePoint = point; + emit trackingImagePointChanged(); + } + if (!qFuzzyCompare(_trackingImageRadius, radius)) { + _trackingImageRadius = radius; + emit trackingImageRadiusChanged(); + } + } else { + // Rectangle tracking + const QRectF rect = QRectF(QPointF(std::clamp(static_cast(_trackingImageStatus.rec_top_x), 0.0, 1.0), + std::clamp(static_cast(_trackingImageStatus.rec_top_y), 0.0, 1.0)), + QPointF(std::clamp(static_cast(_trackingImageStatus.rec_bottom_x), 0.0, 1.0), + std::clamp(static_cast(_trackingImageStatus.rec_bottom_y), 0.0, 1.0))).normalized(); + qCDebug(CameraControlLog) << "Tracking Rect [" << rect << "]"; + _trackingImagePoint = {}; + _trackingImageRadius = 0.0; + if (_trackingImageRect != rect) { + _trackingImageRect = rect; + emit trackingImageRectChanged(); } - // get rectangle into [0..1] boundaries - _trackingImageRect.setLeft(std::min(std::max(_trackingImageRect.left(), 0.0), 1.0)); - _trackingImageRect.setTop(std::min(std::max(_trackingImageRect.top(), 0.0), 1.0)); - _trackingImageRect.setRight(std::min(std::max(_trackingImageRect.right(), 0.0), 1.0)); - _trackingImageRect.setBottom(std::min(std::max(_trackingImageRect.bottom(), 0.0), 1.0)); - - qCDebug(CameraControlLog) << "Tracking Image Status [left:" << _trackingImageRect.left() - << "top:" << _trackingImageRect.top() - << "right:" << _trackingImageRect.right() - << "bottom:" << _trackingImageRect.bottom() << "]"; } - emit trackingImageStatusChanged(); + if (_trackingImageIsActive != active) { + _trackingImageIsActive = active; + emit trackingImageIsActiveChanged(); + } + if (_trackingImageIsPoint != isPoint) { + _trackingImageIsPoint = isPoint; + emit trackingImageIsPointChanged(); + } } void VehicleCameraControl::setCurrentStream(int stream) @@ -2323,16 +2340,23 @@ VehicleCameraControl::mode() void VehicleCameraControl::setTrackingEnabled(bool set) { - if(set) { - _trackingStatus = static_cast(_trackingStatus | TRACKING_ENABLED); - } else { - _trackingStatus = static_cast(_trackingStatus & ~TRACKING_ENABLED); + if (_trackingEnabled == set) { + return; + } + _trackingEnabled = set; + if (!set) { + stopTracking(); } emit trackingEnabledChanged(); } -void VehicleCameraControl::startTracking(QRectF rec) +void VehicleCameraControl::startTrackingRect(QRectF rec) { + if (!_hasTrackingRectCapability) { + qCCritical(CameraControlLog) << "startTrackingRect called but camera does not have rectangle tracking capability"; + return; + } + qCDebug(CameraControlLog) << "Start Tracking (Rectangle: [" << static_cast(rec.x()) << ", " << static_cast(rec.y()) << "] - [" @@ -2350,8 +2374,13 @@ void VehicleCameraControl::startTracking(QRectF rec) _requestTrackingStatus(); } -void VehicleCameraControl::startTracking(QPointF point, double radius) +void VehicleCameraControl::startTrackingPoint(QPointF point, double radius) { + if (!_hasTrackingPointCapability) { + qCCritical(CameraControlLog) << "startTrackingPoint called but camera does not have point tracking capability"; + return; + } + qCDebug(CameraControlLog) << "Start Tracking (Point: [" << static_cast(point.x()) << ", " << static_cast(point.y()) << "], Radius: " @@ -2383,8 +2412,18 @@ void VehicleCameraControl::stopTracking() MAVLINK_MSG_ID_CAMERA_TRACKING_IMAGE_STATUS, -1); - // reset tracking image rectangle + // reset tracking state _trackingImageRect = {}; + _trackingImagePoint = {}; + _trackingImageRadius = 0.0; + if (_trackingImageIsActive) { + _trackingImageIsActive = false; + emit trackingImageIsActiveChanged(); + } + if (_trackingImageIsPoint) { + _trackingImageIsPoint = false; + emit trackingImageIsPointChanged(); + } } void VehicleCameraControl::_requestTrackingStatus() diff --git a/src/Camera/VehicleCameraControl.h b/src/Camera/VehicleCameraControl.h index 041ed430d00a..6ff3470eded6 100644 --- a/src/Camera/VehicleCameraControl.h +++ b/src/Camera/VehicleCameraControl.h @@ -56,8 +56,8 @@ class VehicleCameraControl : public MavlinkCameraControlInterface Q_INVOKABLE virtual void stopZoom (); Q_INVOKABLE virtual void stopStream (); Q_INVOKABLE virtual void resumeStream (); - Q_INVOKABLE virtual void startTracking (QRectF rec); - Q_INVOKABLE virtual void startTracking (QPointF point, double radius); + Q_INVOKABLE virtual void startTrackingRect (QRectF rec); + Q_INVOKABLE virtual void startTrackingPoint (QPointF point, double radius); Q_INVOKABLE virtual void stopTracking (); virtual int version () const { return _version; } @@ -72,7 +72,9 @@ class VehicleCameraControl : public MavlinkCameraControlInterface virtual bool hasModes () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_MODES; } virtual bool hasZoom () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_BASIC_ZOOM; } virtual bool hasFocus () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_BASIC_FOCUS; } - virtual bool hasTracking () const { return _trackingStatus & TRACKING_SUPPORTED; } + virtual bool hasTracking () const { return _hasTrackingRectCapability || _hasTrackingPointCapability; } + virtual bool supportsTrackingPoint() const { return _hasTrackingPointCapability; } + virtual bool supportsTrackingRect () const { return _hasTrackingRectCapability; } virtual bool hasVideoStream () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM; } virtual bool photosInVideoMode () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_CAN_CAPTURE_IMAGE_IN_VIDEO_MODE; } virtual bool videoInPhotoMode () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_CAN_CAPTURE_VIDEO_IN_IMAGE_MODE; } @@ -125,13 +127,14 @@ class VehicleCameraControl : public MavlinkCameraControlInterface virtual void handleVideoStreamInformation(const mavlink_video_stream_information_t &videoStreamInformation); virtual void handleVideoStreamStatus(const mavlink_video_stream_status_t &videoStreamStatus); - virtual bool trackingEnabled () const { return _trackingStatus & TRACKING_ENABLED; } + virtual bool trackingEnabled () const { return _trackingEnabled; } virtual void setTrackingEnabled (bool set); - virtual TrackingStatus trackingStatus () const { return _trackingStatus; } - - virtual bool trackingImageStatus() const { return _trackingImageStatus.tracking_status == 1; } + virtual bool trackingImageIsActive() const { return _trackingImageIsActive; } + virtual bool trackingImageIsPoint() const { return _trackingImageIsPoint; } virtual QRectF trackingImageRect() const { return _trackingImageRect; } + virtual QPointF trackingImagePoint() const { return _trackingImagePoint; } + virtual qreal trackingImageRadius() const { return _trackingImageRadius; } virtual Fact* exposureMode (); virtual Fact* ev (); @@ -293,7 +296,13 @@ protected slots: QStringList _streamLabels; ThermalViewMode _thermalMode = THERMAL_BLEND; double _thermalOpacity = 85.0; - TrackingStatus _trackingStatus = TRACKING_UNKNOWN; - mavlink_camera_tracking_image_status_t _trackingImageStatus; + bool _hasTrackingRectCapability = false; + bool _hasTrackingPointCapability = false; + bool _trackingEnabled = false; + bool _trackingImageIsActive = false; + bool _trackingImageIsPoint = false; + mavlink_camera_tracking_image_status_t _trackingImageStatus{}; QRectF _trackingImageRect; + QPointF _trackingImagePoint; + qreal _trackingImageRadius = 0.0; }; diff --git a/src/Comms/MockLink/MockConfiguration.cc b/src/Comms/MockLink/MockConfiguration.cc index 55052aea96ab..5b03cd0d91d6 100644 --- a/src/Comms/MockLink/MockConfiguration.cc +++ b/src/Comms/MockLink/MockConfiguration.cc @@ -92,8 +92,8 @@ void MockConfiguration::loadSettings(QSettings &settings, const QString &root) setCameraCanCaptureImageInVideoMode(settings.value(_cameraCanCaptureImageInVideoModeKey, true).toBool()); setCameraCanCaptureVideoInImageMode(settings.value(_cameraCanCaptureVideoInImageModeKey, false).toBool()); setCameraHasBasicZoom(settings.value(_cameraHasBasicZoomKey, true).toBool()); - setCameraHasTrackingPoint(settings.value(_cameraHasTrackingPointKey, false).toBool()); - setCameraHasTrackingRectangle(settings.value(_cameraHasTrackingRectangleKey, false).toBool()); + setCameraHasTrackingPoint(settings.value(_cameraHasTrackingPointKey, true).toBool()); + setCameraHasTrackingRectangle(settings.value(_cameraHasTrackingRectangleKey, true).toBool()); setGimbalHasRollAxis(settings.value(_gimbalHasRollAxisKey, true).toBool()); setGimbalHasPitchAxis(settings.value(_gimbalHasPitchAxisKey, true).toBool()); setGimbalHasYawAxis(settings.value(_gimbalHasYawAxisKey, true).toBool()); diff --git a/src/Comms/MockLink/MockConfiguration.h b/src/Comms/MockLink/MockConfiguration.h index 26057d8049c0..ec160a687a26 100644 --- a/src/Comms/MockLink/MockConfiguration.h +++ b/src/Comms/MockLink/MockConfiguration.h @@ -153,8 +153,8 @@ class MockConfiguration : public LinkConfiguration bool _cameraCanCaptureImageInVideoMode = true; bool _cameraCanCaptureVideoInImageMode = false; bool _cameraHasBasicZoom = true; - bool _cameraHasTrackingPoint = false; - bool _cameraHasTrackingRectangle = false; + bool _cameraHasTrackingPoint = true; + bool _cameraHasTrackingRectangle = true; // Gimbal capability flags (defaults - all enabled) bool _gimbalHasRollAxis = true; diff --git a/src/Comms/MockLink/MockLinkCamera.cc b/src/Comms/MockLink/MockLinkCamera.cc index feeb5db2ea36..2cc680e65ab5 100644 --- a/src/Comms/MockLink/MockLinkCamera.cc +++ b/src/Comms/MockLink/MockLinkCamera.cc @@ -5,6 +5,9 @@ #include #include +#include + +#include QGC_LOGGING_CATEGORY(MockLinkCameraLog, "Comms.MockLink.MockLinkCamera") @@ -105,6 +108,34 @@ void MockLinkCamera::run10HzTasks() _sendCameraImageCaptured(cam->compId); _sendCameraCaptureStatus(cam->compId); } + + // Send periodic tracking image status (with simulated drift) + if (cam->trackingMode != CAMERA_TRACKING_MODE_NONE && cam->trackingStatusIntervalUs > 0) { + const qint64 intervalMs = (cam->trackingStatusIntervalUs + 999) / 1000; + if (cam->trackingStatusLastSentMs == 0 || (now - cam->trackingStatusLastSentMs) >= intervalMs) { + // Drift the tracked target in a figure-8 pattern around its anchor + const double elapsed = static_cast(now - cam->trackingStartMs) / 1000.0; + const float driftX = 0.05f * static_cast(qSin(elapsed * 0.7)); + const float driftY = 0.05f * static_cast(qSin(elapsed * 1.1)); + + if (cam->trackingMode == CAMERA_TRACKING_MODE_POINT) { + cam->trackPointX = std::clamp(cam->trackAnchorX + driftX, 0.0f, 1.0f); + cam->trackPointY = std::clamp(cam->trackAnchorY + driftY, 0.0f, 1.0f); + } else { + const float halfW = (cam->trackRecBottomX - cam->trackRecTopX) / 2.0f; + const float halfH = (cam->trackRecBottomY - cam->trackRecTopY) / 2.0f; + const float cx = std::clamp(cam->trackAnchorX + driftX, halfW, 1.0f - halfW); + const float cy = std::clamp(cam->trackAnchorY + driftY, halfH, 1.0f - halfH); + cam->trackRecTopX = cx - halfW; + cam->trackRecTopY = cy - halfH; + cam->trackRecBottomX = cx + halfW; + cam->trackRecBottomY = cy + halfH; + } + + _sendCameraTrackingImageStatus(cam->compId); + cam->trackingStatusLastSentMs = now; + } + } } } @@ -344,12 +375,64 @@ bool MockLinkCamera::_handleCameraCommand(const mavlink_command_long_t &request, return true; case MAV_CMD_CAMERA_TRACK_POINT: + if (cam->capFlags & CAMERA_CAP_FLAGS_HAS_TRACKING_POINT) { + cam->trackingMode = CAMERA_TRACKING_MODE_POINT; + cam->trackPointX = request.param1; + cam->trackPointY = request.param2; + cam->trackRadius = request.param3; + cam->trackAnchorX = request.param1; + cam->trackAnchorY = request.param2; + cam->trackingStartMs = QDateTime::currentMSecsSinceEpoch(); + qCDebug(MockLinkCameraLog) << "Camera" << targetCompId << "tracking point" + << cam->trackPointX << cam->trackPointY << "radius" << cam->trackRadius; + _sendCommandAck(targetCompId, request.command, MAV_RESULT_ACCEPTED); + } else { + _sendCommandAck(targetCompId, request.command, MAV_RESULT_DENIED); + } + return true; + case MAV_CMD_CAMERA_TRACK_RECTANGLE: + if (cam->capFlags & CAMERA_CAP_FLAGS_HAS_TRACKING_RECTANGLE) { + cam->trackingMode = CAMERA_TRACKING_MODE_RECTANGLE; + cam->trackRecTopX = request.param1; + cam->trackRecTopY = request.param2; + cam->trackRecBottomX = request.param3; + cam->trackRecBottomY = request.param4; + cam->trackAnchorX = (request.param1 + request.param3) / 2.0f; + cam->trackAnchorY = (request.param2 + request.param4) / 2.0f; + cam->trackingStartMs = QDateTime::currentMSecsSinceEpoch(); + qCDebug(MockLinkCameraLog) << "Camera" << targetCompId << "tracking rectangle" + << cam->trackRecTopX << cam->trackRecTopY + << "->" << cam->trackRecBottomX << cam->trackRecBottomY; + _sendCommandAck(targetCompId, request.command, MAV_RESULT_ACCEPTED); + } else { + _sendCommandAck(targetCompId, request.command, MAV_RESULT_DENIED); + } + return true; + case MAV_CMD_CAMERA_STOP_TRACKING: - qCDebug(MockLinkCameraLog) << "Camera" << targetCompId << "tracking command" << request.command; + cam->trackingMode = CAMERA_TRACKING_MODE_NONE; + cam->trackingStatusIntervalUs = -1; + cam->trackingStatusLastSentMs = 0; + qCDebug(MockLinkCameraLog) << "Camera" << targetCompId << "tracking stopped"; _sendCommandAck(targetCompId, request.command, MAV_RESULT_ACCEPTED); return true; + case MAV_CMD_SET_MESSAGE_INTERVAL: + { + const int msgId = static_cast(request.param1); + if (msgId == MAVLINK_MSG_ID_CAMERA_TRACKING_IMAGE_STATUS) { + cam->trackingStatusIntervalUs = static_cast(request.param2); + cam->trackingStatusLastSentMs = 0; + qCDebug(MockLinkCameraLog) << "Camera" << targetCompId + << "tracking status interval" << cam->trackingStatusIntervalUs << "us"; + _sendCommandAck(targetCompId, request.command, MAV_RESULT_ACCEPTED); + return true; + } + _sendCommandAck(targetCompId, request.command, MAV_RESULT_UNSUPPORTED); + return true; + } + default: break; } @@ -650,6 +733,33 @@ void MockLinkCamera::_sendVideoStreamStatus(uint8_t compId, uint8_t streamId) qCDebug(MockLinkCameraLog) << "Sent VIDEO_STREAM_STATUS for compId:" << compId << "stream:" << streamId; } +void MockLinkCamera::_sendCameraTrackingImageStatus(uint8_t compId) +{ + const CameraState *cam = _findCamera(compId); + if (!cam || cam->trackingMode == CAMERA_TRACKING_MODE_NONE) { + return; + } + + mavlink_message_t msg{}; + (void) mavlink_msg_camera_tracking_image_status_pack_chan( + _mockLink->vehicleId(), + compId, + _mockLink->mavlinkChannel(), + &msg, + CAMERA_TRACKING_STATUS_FLAGS_ACTIVE, // tracking_status + cam->trackingMode, // tracking_mode + CAMERA_TRACKING_TARGET_DATA_EMBEDDED, // target_data + cam->trackPointX, // point_x + cam->trackPointY, // point_y + cam->trackRadius, // radius + cam->trackRecTopX, // rec_top_x + cam->trackRecTopY, // rec_top_y + cam->trackRecBottomX, // rec_bottom_x + cam->trackRecBottomY, // rec_bottom_y + 0); // camera_device_id + _mockLink->respondWithMavlinkMessage(msg); +} + void MockLinkCamera::_sendCommandAck(uint8_t compId, uint16_t command, uint8_t result, int requestedMsgId) { mavlink_message_t msg{}; diff --git a/src/Comms/MockLink/MockLinkCamera.h b/src/Comms/MockLink/MockLinkCamera.h index f7e9a1eadbcf..d489449c02ad 100644 --- a/src/Comms/MockLink/MockLinkCamera.h +++ b/src/Comms/MockLink/MockLinkCamera.h @@ -57,6 +57,21 @@ class MockLinkCamera uint8_t image_status = ImageCaptureIdle; ///< ImageCaptureStatus enum float image_interval = 0.0f; ///< Interval between image captures (seconds) qint64 singleShotStartMs = 0; ///< Timestamp when single-shot capture started (0 = not active) + + // Tracking state + uint8_t trackingMode = CAMERA_TRACKING_MODE_NONE; ///< CAMERA_TRACKING_MODE enum + float trackPointX = 0.0f; + float trackPointY = 0.0f; + float trackRadius = 0.0f; + float trackRecTopX = 0.0f; + float trackRecTopY = 0.0f; + float trackRecBottomX = 0.0f; + float trackRecBottomY = 0.0f; + qint64 trackingStatusIntervalUs = -1; ///< Interval for CAMERA_TRACKING_IMAGE_STATUS (-1 = disabled) + qint64 trackingStatusLastSentMs = 0; ///< Timestamp of last tracking status message + qint64 trackingStartMs = 0; ///< Timestamp when tracking was started (for drift animation) + float trackAnchorX = 0.0f; ///< Original center X of tracked target + float trackAnchorY = 0.0f; ///< Original center Y of tracked target }; explicit MockLinkCamera(MockLink *mockLink, @@ -97,6 +112,7 @@ class MockLinkCamera void _sendCameraImageCaptured(uint8_t compId); void _sendVideoStreamInformation(uint8_t compId, uint8_t streamId); void _sendVideoStreamStatus(uint8_t compId, uint8_t streamId); + void _sendCameraTrackingImageStatus(uint8_t compId); void _sendCommandAck(uint8_t compId, uint16_t command, uint8_t result, int requestedMsgId = -1); CameraState *_findCamera(uint8_t compId); diff --git a/src/FlyView/CMakeLists.txt b/src/FlyView/CMakeLists.txt index 2f77718c32fe..752b54e294ac 100644 --- a/src/FlyView/CMakeLists.txt +++ b/src/FlyView/CMakeLists.txt @@ -52,6 +52,7 @@ qt_add_qml_module(FlyViewModule ObstacleDistanceOverlay.qml ObstacleDistanceOverlayMap.qml ObstacleDistanceOverlayVideo.qml + OnScreenCameraTrackingController.qml OnScreenGimbalController.qml PreFlightBatteryCheck.qml PreFlightCheckList.qml diff --git a/src/FlyView/FlyViewVideo.qml b/src/FlyView/FlyViewVideo.qml index f59d9cc92250..0b5e95a465a1 100644 --- a/src/FlyView/FlyViewVideo.qml +++ b/src/FlyView/FlyViewVideo.qml @@ -9,9 +9,6 @@ Item { property Item pipView property Item pipState: videoPipState - property int _track_rec_x: 0 - property int _track_rec_y: 0 - PipState { id: videoPipState pipView: _root.pipView @@ -53,7 +50,7 @@ Item { QGCLabel { text: qsTr("Double-click to exit full screen") font.pointSize: ScreenTools.largeFontPointSize - visible: QGroundControl.videoManager.fullScreen && flyViewVideoMouseArea.containsMouse + visible: QGroundControl.videoManager.fullScreen anchors.centerIn: parent onVisibleChanged: { @@ -74,159 +71,56 @@ Item { OnScreenGimbalController { id: onScreenGimbalController anchors.fill: parent - screenX: flyViewVideoMouseArea.mouseX - screenY: flyViewVideoMouseArea.mouseY - cameraTrackingEnabled: videoStreaming._camera && videoStreaming._camera.trackingEnabled + cameraTrackingEnabled: !!(videoStreaming._camera && videoStreaming._camera.trackingEnabled) + } + + OnScreenCameraTrackingController { + id: cameraTrackingController + anchors.fill: parent + camera: videoStreaming._camera + videoWidth: videoStreaming.getWidth() + videoHeight: videoStreaming.getHeight() } MouseArea { id: flyViewVideoMouseArea anchors.fill: parent enabled: pipState.state === pipState.fullState - hoverEnabled: true - - property double x0: 0 - property double x1: 0 - property double y0: 0 - property double y1: 0 - property double offset_x: 0 - property double offset_y: 0 - property double radius: 20 - property var trackingROI: null - property var trackingStatus: trackingStatusComponent.createObject(flyViewVideoMouseArea, {}) - - onClicked: onScreenGimbalController.clickControl() - onDoubleClicked: QGroundControl.videoManager.fullScreen = !QGroundControl.videoManager.fullScreen - - onPressed:(mouse) => { - onScreenGimbalController.pressControl() - _track_rec_x = mouse.x - _track_rec_y = mouse.y - - //create a new rectangle at the wanted position - if(videoStreaming._camera) { - if (videoStreaming._camera.trackingEnabled) { - trackingROI = trackingROIComponent.createObject(flyViewVideoMouseArea, { - "x": mouse.x, - "y": mouse.y - }); - } - } - } - onPositionChanged: (mouse) => { - //on move, update the width of rectangle - if (trackingROI !== null) { - if (mouse.x < trackingROI.x) { - trackingROI.x = mouse.x - trackingROI.width = Math.abs(mouse.x - _track_rec_x) - } else { - trackingROI.width = Math.abs(mouse.x - trackingROI.x) - } - if (mouse.y < trackingROI.y) { - trackingROI.y = mouse.y - trackingROI.height = Math.abs(mouse.y - _track_rec_y) - } else { - trackingROI.height = Math.abs(mouse.y - trackingROI.y) - } - } - } - onReleased: (mouse) => { - onScreenGimbalController.releaseControl() + property real _pressX: 0 + property real _pressY: 0 + property bool _dragging: false + readonly property real _dragThreshold: 10 - //if there is already a selection, delete it - if (trackingROI !== null) { - trackingROI.destroy(); - } + onDoubleClicked: QGroundControl.videoManager.fullScreen = !QGroundControl.videoManager.fullScreen - if(videoStreaming._camera) { - if (videoStreaming._camera.trackingEnabled) { - // order coordinates --> top/left and bottom/right - x0 = Math.min(_track_rec_x, mouse.x) - x1 = Math.max(_track_rec_x, mouse.x) - y0 = Math.min(_track_rec_y, mouse.y) - y1 = Math.max(_track_rec_y, mouse.y) - - //calculate offset between video stream rect and background (black stripes) - offset_x = (parent.width - videoStreaming.getWidth()) / 2 - offset_y = (parent.height - videoStreaming.getHeight()) / 2 - - //convert absolute coords in background to absolute video stream coords - x0 = x0 - offset_x - x1 = x1 - offset_x - y0 = y0 - offset_y - y1 = y1 - offset_y - - //convert absolute to relative coordinates and limit range to 0...1 - x0 = Math.max(Math.min(x0 / videoStreaming.getWidth(), 1.0), 0.0) - x1 = Math.max(Math.min(x1 / videoStreaming.getWidth(), 1.0), 0.0) - y0 = Math.max(Math.min(y0 / videoStreaming.getHeight(), 1.0), 0.0) - y1 = Math.max(Math.min(y1 / videoStreaming.getHeight(), 1.0), 0.0) - - //use point message if rectangle is very small - if (Math.abs(_track_rec_x - mouse.x) < 10 && Math.abs(_track_rec_y - mouse.y) < 10) { - var pt = Qt.point(x0, y0) - videoStreaming._camera.startTracking(pt, radius / videoStreaming.getWidth()) - } else { - var rec = Qt.rect(x0, y0, x1 - x0, y1 - y0) - videoStreaming._camera.startTracking(rec) - } - _track_rec_x = 0 - _track_rec_y = 0 - } - } + onPressed: (mouse) => { + _pressX = mouse.x + _pressY = mouse.y + _dragging = false } - Component { - id: trackingROIComponent - - Rectangle { - color: Qt.rgba(0.1,0.85,0.1,0.25) - border.color: "green" - border.width: 1 + onPositionChanged: (mouse) => { + if (!_dragging && (Math.abs(mouse.x - _pressX) >= _dragThreshold || Math.abs(mouse.y - _pressY) >= _dragThreshold)) { + _dragging = true + onScreenGimbalController.mouseDragStart(_pressX, _pressY) + cameraTrackingController.mouseDragStart(_pressX, _pressY) } - } - - Component { - id: trackingStatusComponent - - Rectangle { - color: "transparent" - border.color: "red" - border.width: 5 - radius: 5 + if (_dragging) { + onScreenGimbalController.mouseDragPositionChanged(mouse.x, mouse.y) + cameraTrackingController.mouseDragPositionChanged(mouse.x, mouse.y) } } - Timer { - id: trackingStatusTimer - interval: 50 - repeat: true - running: true - onTriggered: { - if (videoStreaming._camera) { - if (videoStreaming._camera.trackingEnabled && videoStreaming._camera.trackingImageStatus) { - var margin_hor = (parent.parent.width - videoStreaming.getWidth()) / 2 - var margin_ver = (parent.parent.height - videoStreaming.getHeight()) / 2 - var left = margin_hor + videoStreaming.getWidth() * videoStreaming._camera.trackingImageRect.left - var top = margin_ver + videoStreaming.getHeight() * videoStreaming._camera.trackingImageRect.top - var right = margin_hor + videoStreaming.getWidth() * videoStreaming._camera.trackingImageRect.right - var bottom = margin_ver + !isNaN(videoStreaming._camera.trackingImageRect.bottom) ? videoStreaming.getHeight() * videoStreaming._camera.trackingImageRect.bottom : top + (right - left) - var width = right - left - var height = bottom - top - - flyViewVideoMouseArea.trackingStatus.x = left - flyViewVideoMouseArea.trackingStatus.y = top - flyViewVideoMouseArea.trackingStatus.width = width - flyViewVideoMouseArea.trackingStatus.height = height - } else { - flyViewVideoMouseArea.trackingStatus.x = 0 - flyViewVideoMouseArea.trackingStatus.y = 0 - flyViewVideoMouseArea.trackingStatus.width = 0 - flyViewVideoMouseArea.trackingStatus.height = 0 - } - } + onReleased: (mouse) => { + if (_dragging) { + onScreenGimbalController.mouseDragEnd() + cameraTrackingController.mouseDragEnd(mouse.x, mouse.y) + } else { + onScreenGimbalController.mouseClicked(mouse.x, mouse.y) + cameraTrackingController.mouseClicked(mouse.x, mouse.y) } + _dragging = false } } diff --git a/src/FlyView/OnScreenCameraTrackingController.qml b/src/FlyView/OnScreenCameraTrackingController.qml new file mode 100644 index 000000000000..cb6182358ace --- /dev/null +++ b/src/FlyView/OnScreenCameraTrackingController.qml @@ -0,0 +1,150 @@ +import QtQuick + +Item { + id: rootItem + + required property var camera + required property real videoWidth + required property real videoHeight + + // Drag origin in parent coordinates + property real _dragStartX: 0 + property real _dragStartY: 0 + property bool _dragging: false + property real _dragCurrentX: 0 + property real _dragCurrentY: 0 + + readonly property bool _trackingEnabled: camera && camera.trackingEnabled + readonly property bool _canTrackPoint: _trackingEnabled && camera.supportsTrackingPoint + readonly property bool _canTrackRect: _trackingEnabled && camera.supportsTrackingRect + + function mouseClicked(mouseX, mouseY) { + if (!_canTrackPoint) { + return + } + if (videoWidth <= 0 || videoHeight <= 0) { + return + } + // Click = point tracking + var marginH = (width - videoWidth) / 2 + var marginV = (height - videoHeight) / 2 + var nx = Math.max(Math.min((mouseX - marginH) / videoWidth, 1.0), 0.0) + var ny = Math.max(Math.min((mouseY - marginV) / videoHeight, 1.0), 0.0) + var pointRadius = 20 + camera.startTrackingPoint(Qt.point(nx, ny), Math.min(pointRadius / videoWidth, 1.0)) + } + + function mouseDragStart(mouseX, mouseY) { + if (!_canTrackRect) { + return + } + _dragStartX = mouseX + _dragStartY = mouseY + _dragCurrentX = mouseX + _dragCurrentY = mouseY + _dragging = true + } + + function mouseDragPositionChanged(mouseX, mouseY) { + if (!_dragging) { + return + } + _dragCurrentX = mouseX + _dragCurrentY = mouseY + } + + function mouseDragEnd(mouseX, mouseY) { + _dragging = false + + if (!_canTrackRect) { + return + } + if (videoWidth <= 0 || videoHeight <= 0) { + return + } + + // Order coordinates: top-left and bottom-right + var x0 = Math.min(_dragStartX, mouseX) + var x1 = Math.max(_dragStartX, mouseX) + var y0 = Math.min(_dragStartY, mouseY) + var y1 = Math.max(_dragStartY, mouseY) + + // Letterbox margins (black bars when aspect ratio doesn't match) + var marginH = (width - videoWidth) / 2 + var marginV = (height - videoHeight) / 2 + + // Convert from view coordinates to video-relative coordinates + x0 = x0 - marginH + x1 = x1 - marginH + y0 = y0 - marginV + y1 = y1 - marginV + + // Normalize to 0..1 + x0 = Math.max(Math.min(x0 / videoWidth, 1.0), 0.0) + x1 = Math.max(Math.min(x1 / videoWidth, 1.0), 0.0) + y0 = Math.max(Math.min(y0 / videoHeight, 1.0), 0.0) + y1 = Math.max(Math.min(y1 / videoHeight, 1.0), 0.0) + + var w = x1 - x0 + var h = y1 - y0 + + // Ignore degenerate rectangles (e.g. mostly-horizontal/vertical drags) + if (w < 0.01 || h < 0.01) { + _dragStartX = 0 + _dragStartY = 0 + return + } + + // Drag = rectangle tracking + camera.startTrackingRect(Qt.rect(x0, y0, w, h)) + + _dragStartX = 0 + _dragStartY = 0 + } + + // --- ROI selection overlay (green rectangle while dragging) --- + Rectangle { + visible: _dragging + color: Qt.rgba(0.1, 0.85, 0.1, 0.25) + border.color: "green" + border.width: 1 + x: Math.min(_dragStartX, _dragCurrentX) + y: Math.min(_dragStartY, _dragCurrentY) + width: Math.abs(_dragCurrentX - _dragStartX) + height: Math.abs(_dragCurrentY - _dragStartY) + } + + // --- Tracking status overlay (red rectangle/circle from camera feedback) --- + // Coordinates are normalized to the video frame (0.0–1.0). Convert to screen + // pixels by scaling by the displayed video size and offsetting by letterbox margins. + readonly property bool _trackingActive: _trackingEnabled && camera.trackingImageIsActive + readonly property bool _isPoint: _trackingActive && camera.trackingImageIsPoint + readonly property real _marginH: (rootItem.width - videoWidth) / 2 + readonly property real _marginV: (rootItem.height - videoHeight) / 2 + + Rectangle { + id: trackingStatusOverlay + color: "transparent" + border.color: "red" + border.width: 5 + radius: rootItem._isPoint ? width / 2 : 5 + visible: rootItem._trackingActive + + x: rootItem._trackingActive + ? (rootItem._isPoint ? rootItem._marginH + videoWidth * (camera.trackingImagePoint.x - camera.trackingImageRadius) + : rootItem._marginH + videoWidth * camera.trackingImageRect.x) + : 0 + y: rootItem._trackingActive + ? (rootItem._isPoint ? rootItem._marginV + videoHeight * (camera.trackingImagePoint.y - camera.trackingImageRadius) + : rootItem._marginV + videoHeight * camera.trackingImageRect.y) + : 0 + width: rootItem._trackingActive + ? (rootItem._isPoint ? videoWidth * camera.trackingImageRadius * 2 + : videoWidth * camera.trackingImageRect.width) + : 0 + height: rootItem._trackingActive + ? (rootItem._isPoint ? width + : videoHeight * camera.trackingImageRect.height) + : 0 + } +} diff --git a/src/FlyView/OnScreenGimbalController.qml b/src/FlyView/OnScreenGimbalController.qml index 08d015f30636..dedd1c678296 100644 --- a/src/FlyView/OnScreenGimbalController.qml +++ b/src/FlyView/OnScreenGimbalController.qml @@ -4,86 +4,70 @@ import QGroundControl import QGroundControl.Controls Item { - id: rootItem - anchors.fill: parent + id: rootItem + anchors.fill: parent - property var screenX - property var screenY - property var screenXrateInitCoocked - property var screenYrateInitCoocked + required property bool cameraTrackingEnabled - property var activeVehicle: QGroundControl.multiVehicleManager.activeVehicle - property var gimbalController: activeVehicle ? activeVehicle.gimbalController : undefined - property var activeGimbal: gimbalController ? gimbalController.activeGimbal : undefined - property bool gimbalAvailable: activeGimbal != undefined - property var gimbalControllerSettings: QGroundControl.settingsManager.gimbalControllerSettings - property bool cameraTrackingEnabled: false // Used to ignore clicks when camera tracking operation is active, otherwise it would collide with these gimbal controls - property bool shouldProcessClicks: gimbalControllerSettings.EnableOnScreenControl.value && activeGimbal && !cameraTrackingEnabled ? true : false + property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle + property var _gimbalController: _activeVehicle ? _activeVehicle.gimbalController : undefined + property var _activeGimbal: _gimbalController ? _gimbalController.activeGimbal : undefined + property bool _gimbalAvailable: _activeGimbal != undefined + property var _gimbalControllerSettings: QGroundControl.settingsManager.gimbalControllerSettings + property bool _shouldProcessClicks: _gimbalControllerSettings.enableOnScreenControl.value && _activeGimbal && !cameraTrackingEnabled ? true : false - function clickControl() { - if (!shouldProcessClicks) { + property real _mouseX: 0 + property real _mouseY: 0 + property real _dragStartNormX: 0 + property real _dragStartNormY: 0 + + function _toNormX(mouseX) { return ((mouseX / width) * 2) - 1 } + function _toNormY(mouseY) { return -((mouseY / height) * 2) + 1 } + + function mouseClicked(mouseX, mouseY) { + if (!_shouldProcessClicks) { return } - // If click and slide control, return, it uses press and release - if (!gimbalControllerSettings.ControlType.rawValue == 0) { + if (_gimbalControllerSettings.clickAndDrag.rawValue) { return } - clickAndPoint(x, y) - } - - // Sends a +-(0-1) xy value to vehicle.gimbalController.gimbalOnScreenControl - function clickAndPoint() { - if (rootItem.gimbalAvailable) { - var xCoocked = ( (screenX / parent.width) * 2) - 1 - var yCoocked = -( (screenY / parent.height) * 2) + 1 - // console.log("X global: " + x + " Y global: " + y) - // console.log("X coocked: " + xCoocked + " Y coocked: " + yCoocked) - gimbalController.gimbalOnScreenControl(xCoocked, yCoocked, true, false, false) - } else { - // We should never be here - console.log("gimbal not available") + if (rootItem._gimbalAvailable) { + _gimbalController.gimbalOnScreenControl(_toNormX(mouseX), _toNormY(mouseY), true, false, false) } } - function pressControl() { - if (!shouldProcessClicks) { + function mouseDragStart(mouseX, mouseY) { + if (!_shouldProcessClicks) { return } - // If click and point control return, that is handled exclusively on clickAndPoint() - if (!gimbalControllerSettings.ControlType.rawValue == 1) { + if (!_gimbalControllerSettings.clickAndDrag.rawValue) { return } + _mouseX = mouseX + _mouseY = mouseY + _dragStartNormX = _toNormX(mouseX) + _dragStartNormY = _toNormY(mouseY) sendRateTimer.start() - screenXrateInitCoocked = ( ( screenX / parent.width) * 2) - 1 - screenYrateInitCoocked = -( ( screenY / parent.height) * 2) + 1 } - function releaseControl() { - if (!shouldProcessClicks) { - return - } - // If click and point control return, that is handled exclusively on clickAndPoint() - if (!gimbalControllerSettings.ControlType.rawValue == 1) { - return - } + function mouseDragPositionChanged(mouseX, mouseY) { + _mouseX = mouseX + _mouseY = mouseY + } + + function mouseDragEnd() { sendRateTimer.stop() - screenXrateInitCoocked = null - screenYrateInitCoocked = null } Timer { - id: sendRateTimer - interval: 100 - repeat: true + id: sendRateTimer + interval: 100 + repeat: true onTriggered: { - if (rootItem.gimbalAvailable) { - var xCoocked = ( ( screenX / parent.width) * 2) - 1 - var yCoocked = -( ( screenY / parent.height) * 2) + 1 - xCoocked -= screenXrateInitCoocked - yCoocked -= screenYrateInitCoocked - gimbalController.gimbalOnScreenControl(xCoocked, yCoocked, false, true, true) - } else { - console.log("gimbal not available") + if (rootItem._gimbalAvailable) { + var dx = rootItem._toNormX(rootItem._mouseX) - rootItem._dragStartNormX + var dy = rootItem._toNormY(rootItem._mouseY) - rootItem._dragStartNormY + _gimbalController.gimbalOnScreenControl(dx, dy, false, true, true) } } } diff --git a/src/Gimbal/GimbalController.cc b/src/Gimbal/GimbalController.cc index 2122369b41e6..8826cf9baf7b 100644 --- a/src/Gimbal/GimbalController.cc +++ b/src/Gimbal/GimbalController.cc @@ -39,7 +39,7 @@ void GimbalController::_initialConnectCompleted() void GimbalController::setActiveGimbal(Gimbal *gimbal) { if (!gimbal) { - qCDebug(GimbalControllerLog) << "Set active gimbal: attempted to set a nullptr, returning"; + qCCritical(GimbalControllerLog) << "Set active gimbal: attempted to set a nullptr, returning"; return; } @@ -344,7 +344,7 @@ void GimbalController::_checkComplete(Gimbal &gimbal, GimbalPairId pairId) bool GimbalController::_tryGetGimbalControl() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "_tryGetGimbalControl: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "_tryGetGimbalControl: active gimbal is nullptr, returning"; return false; } @@ -375,7 +375,7 @@ bool GimbalController::_yawInVehicleFrame(uint32_t flags) void GimbalController::gimbalPitchStart(int direction) { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalPitchStart: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalPitchStart: active gimbal is nullptr, returning"; return; } @@ -388,7 +388,7 @@ void GimbalController::gimbalPitchStart(int direction) void GimbalController::gimbalYawStart(int direction) { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalYawStart: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalYawStart: active gimbal is nullptr, returning"; return; } @@ -400,7 +400,7 @@ void GimbalController::gimbalYawStart(int direction) void GimbalController::gimbalPitchStop() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalPitchStop: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalPitchStop: active gimbal is nullptr, returning"; return; } @@ -411,7 +411,7 @@ void GimbalController::gimbalPitchStop() void GimbalController::gimbalYawStop() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalYawStop: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalYawStop: active gimbal is nullptr, returning"; return; } @@ -422,7 +422,7 @@ void GimbalController::gimbalYawStop() void GimbalController::centerGimbal() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalYawStep: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalYawStep: active gimbal is nullptr, returning"; return; } sendPitchBodyYaw(0.0, 0.0, true); @@ -433,13 +433,13 @@ void GimbalController::gimbalOnScreenControl(float panPct, float tiltPct, bool c // Pan and tilt comes as +-(0-1) if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalOnScreenControl: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalOnScreenControl: active gimbal is nullptr, returning"; return; } if (clickAndPoint) { // based on FOV - const float hFov = SettingsManager::instance()->gimbalControllerSettings()->CameraHFov()->rawValue().toFloat(); - const float vFov = SettingsManager::instance()->gimbalControllerSettings()->CameraVFov()->rawValue().toFloat(); + const float hFov = SettingsManager::instance()->gimbalControllerSettings()->cameraHFov()->rawValue().toFloat(); + const float vFov = SettingsManager::instance()->gimbalControllerSettings()->cameraVFov()->rawValue().toFloat(); const float panIncDesired = panPct * hFov * 0.5f; const float tiltIncDesired = tiltPct * vFov * 0.5f; @@ -456,7 +456,7 @@ void GimbalController::gimbalOnScreenControl(float panPct, float tiltPct, bool c // Should send rate commands, but it seems for some reason it is not working on AP side. // Pitch works ok but yaw doesn't stop, it keeps like inertia, like if it was buffering the messages. // So we do a workaround with angle targets - const float maxSpeed = SettingsManager::instance()->gimbalControllerSettings()->CameraSlideSpeed()->rawValue().toFloat(); + const float maxSpeed = SettingsManager::instance()->gimbalControllerSettings()->cameraSlideSpeed()->rawValue().toFloat(); const float panIncDesired = panPct * maxSpeed * 0.1f; const float tiltIncDesired = tiltPct * maxSpeed * 0.1f; @@ -686,7 +686,7 @@ void GimbalController::sendPitchYawFlags(uint32_t flags) void GimbalController::acquireGimbalControl() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "acquireGimbalControl: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "acquireGimbalControl: active gimbal is nullptr, returning"; return; } @@ -706,7 +706,7 @@ void GimbalController::acquireGimbalControl() void GimbalController::releaseGimbalControl() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "releaseGimbalControl: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "releaseGimbalControl: active gimbal is nullptr, returning"; return; } diff --git a/src/Settings/GimbalController.SettingsGroup.json b/src/Settings/GimbalController.SettingsGroup.json index 343a2550dfa2..84227e875486 100644 --- a/src/Settings/GimbalController.SettingsGroup.json +++ b/src/Settings/GimbalController.SettingsGroup.json @@ -4,35 +4,33 @@ "QGC.MetaData.Facts": [ { - "name": "EnableOnScreenControl", + "name": "enableOnScreenControl", "shortDesc": "Enable on Screen Camera Control", "type": "bool", "default": false }, { - "name": "ControlType", - "shortDesc": "Type of on-screen control", - "type": "uint32", - "enumStrings": "Click to point, Click and drag", - "enumValues": "0, 1", - "default": 0 + "name": "clickAndDrag", + "shortDesc": "Use click and drag control instead of click to point", + "type": "bool", + "default": false }, { - "name": "CameraVFov", + "name": "cameraVFov", "shortDesc": "Vertical camera field of view", "type": "uint32", "default": 80, "units": "deg" }, { - "name": "CameraHFov", + "name": "cameraHFov", "shortDesc": "Horizontal camera field of view", "type": "uint32", "default": 80, "units": "deg" }, { - "name": "CameraSlideSpeed", + "name": "cameraSlideSpeed", "shortDesc": "Maximum gimbal speed on click and drag (deg/sec)", "type": "uint32", "default": 30, diff --git a/src/Settings/GimbalControllerSettings.cc b/src/Settings/GimbalControllerSettings.cc index 4e16810f49f1..864aec5e3ea5 100644 --- a/src/Settings/GimbalControllerSettings.cc +++ b/src/Settings/GimbalControllerSettings.cc @@ -2,13 +2,31 @@ DECLARE_SETTINGGROUP(GimbalController, "GimbalController") { + // Setting names were changed from PascalCase to camelCase + // Migrate old settings to new names + QSettings settings; + settings.beginGroup(_name); + static const QMap renamedKeys = { + { "EnableOnScreenControl", "enableOnScreenControl" }, + { "ControlType", "clickAndDrag" }, + { "CameraVFov", "cameraVFov" }, + { "CameraHFov", "cameraHFov" }, + { "CameraSlideSpeed", "cameraSlideSpeed" }, + }; + for (auto it = renamedKeys.constBegin(); it != renamedKeys.constEnd(); ++it) { + if (settings.contains(it.key())) { + settings.setValue(it.value(), settings.value(it.key())); + settings.remove(it.key()); + } + } + settings.endGroup(); } -DECLARE_SETTINGSFACT(GimbalControllerSettings, EnableOnScreenControl) -DECLARE_SETTINGSFACT(GimbalControllerSettings, ControlType) -DECLARE_SETTINGSFACT(GimbalControllerSettings, CameraVFov) -DECLARE_SETTINGSFACT(GimbalControllerSettings, CameraHFov) -DECLARE_SETTINGSFACT(GimbalControllerSettings, CameraSlideSpeed) +DECLARE_SETTINGSFACT(GimbalControllerSettings, enableOnScreenControl) +DECLARE_SETTINGSFACT(GimbalControllerSettings, clickAndDrag) +DECLARE_SETTINGSFACT(GimbalControllerSettings, cameraVFov) +DECLARE_SETTINGSFACT(GimbalControllerSettings, cameraHFov) +DECLARE_SETTINGSFACT(GimbalControllerSettings, cameraSlideSpeed) DECLARE_SETTINGSFACT(GimbalControllerSettings, showAzimuthIndicatorOnMap) DECLARE_SETTINGSFACT(GimbalControllerSettings, toolbarIndicatorShowAzimuth) DECLARE_SETTINGSFACT(GimbalControllerSettings, toolbarIndicatorShowAcquireReleaseControl) diff --git a/src/Settings/GimbalControllerSettings.h b/src/Settings/GimbalControllerSettings.h index 27fa38090474..17fa0fe51339 100644 --- a/src/Settings/GimbalControllerSettings.h +++ b/src/Settings/GimbalControllerSettings.h @@ -13,11 +13,11 @@ class GimbalControllerSettings : public SettingsGroup GimbalControllerSettings(QObject* parent = nullptr); DEFINE_SETTING_NAME_GROUP() - DEFINE_SETTINGFACT(EnableOnScreenControl) - DEFINE_SETTINGFACT(ControlType) - DEFINE_SETTINGFACT(CameraVFov) - DEFINE_SETTINGFACT(CameraHFov) - DEFINE_SETTINGFACT(CameraSlideSpeed) + DEFINE_SETTINGFACT(enableOnScreenControl) + DEFINE_SETTINGFACT(clickAndDrag) + DEFINE_SETTINGFACT(cameraVFov) + DEFINE_SETTINGFACT(cameraHFov) + DEFINE_SETTINGFACT(cameraSlideSpeed) DEFINE_SETTINGFACT(showAzimuthIndicatorOnMap) DEFINE_SETTINGFACT(toolbarIndicatorShowAzimuth) DEFINE_SETTINGFACT(toolbarIndicatorShowAcquireReleaseControl) diff --git a/src/UI/toolbar/GimbalIndicator.qml b/src/UI/toolbar/GimbalIndicator.qml index 4f71d4ff58dd..01bd5fc278ed 100644 --- a/src/UI/toolbar/GimbalIndicator.qml +++ b/src/UI/toolbar/GimbalIndicator.qml @@ -235,31 +235,32 @@ Item { id: enableOnScreenControlCheckbox Layout.fillWidth: true text: qsTr("Enabled") - fact: _gimbalControllerSettings.EnableOnScreenControl + fact: _gimbalControllerSettings.enableOnScreenControl } - LabelledFactComboBox { - label: qsTr("Control type") - fact: _gimbalControllerSettings.ControlType - visible: enableOnScreenControlCheckbox.checked + FactCheckBoxSlider { + Layout.fillWidth: true + text: qsTr("Click and drag") + fact: _gimbalControllerSettings.clickAndDrag + visible: enableOnScreenControlCheckbox.checked } LabelledFactTextField { label: qsTr("Horizontal FOV") - fact: _gimbalControllerSettings.CameraHFov - visible: enableOnScreenControlCheckbox.checked && _gimbalControllerSettings.ControlType.rawValue === 0 + fact: _gimbalControllerSettings.cameraHFov + visible: enableOnScreenControlCheckbox.checked && !_gimbalControllerSettings.clickAndDrag.rawValue } LabelledFactTextField { label: qsTr("Vertical FOV") - fact: _gimbalControllerSettings.CameraVFov - visible: enableOnScreenControlCheckbox.checked && _gimbalControllerSettings.ControlType.rawValue === 0 + fact: _gimbalControllerSettings.cameraVFov + visible: enableOnScreenControlCheckbox.checked && !_gimbalControllerSettings.clickAndDrag.rawValue } LabelledFactTextField { label: qsTr("Max speed") - fact: _gimbalControllerSettings.CameraSlideSpeed - visible: enableOnScreenControlCheckbox.checked && _gimbalControllerSettings.ControlType.rawValue === 1 + fact: _gimbalControllerSettings.cameraSlideSpeed + visible: enableOnScreenControlCheckbox.checked && _gimbalControllerSettings.clickAndDrag.rawValue } } From d92bc90e5077578d033634d5be0547e86603cca1 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 18 Mar 2026 14:29:14 -0700 Subject: [PATCH 112/119] Fix MissionController takedown sequencing for Qt 6 TreeView Qt 6 TreeView delegates are destroyed asynchronously during a polish cycle, not synchronously when rows are removed. When MissionController replaced _visualItems, the old C++ VisualMissionItem objects were destroyed before the TreeView polish cycle released delegates still holding QML bindings, causing null-reference TypeErrors. Fix takedown sequencing with stash-init-destroy pattern: - Add _setupNewVisualItems() that stashes the old list, builds the new one, then destroys old items after a 1-second delay via QTimer::singleShot - Refactor removeAll(), _newMissionItemsAvailableFromVehicle(), and _initLoadedVisualItems() to use _setupNewVisualItems() - Make missionItemCount a computed property from _visualItems->count(), removing the _missionItemCount member variable - Replace _updateContainsItems() with direct signal-to-signal connections from countChanged to containsItemsChanged and missionItemCountChanged - Fix connect/disconnect mismatch in _deinitAllVisualItems (was disconnecting _recalcAll but _initAllVisualItems connected _recalcMissionFlightStatus) - Fix PlanMasterController::_showPlanFromManagerVehicle() guard inversion that caused double-call of _newMissionItemsAvailableFromVehicle --- src/MissionManager/MissionController.cc | 107 +++++++++------------ src/MissionManager/MissionController.h | 8 +- src/MissionManager/PlanMasterController.cc | 6 +- 3 files changed, 55 insertions(+), 66 deletions(-) diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index f4395fda2470..8c8ee0736708 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -149,29 +149,17 @@ void MissionController::_newMissionItemsAvailableFromVehicle(bool removeAllReque // - A load from vehicle was manually requested // - The initial automatic load from a vehicle completed and the current editor is empty - _deinitAllVisualItems(); - _visualItems->clearAndDeleteContents(); - _visualItems->deleteLater(); - _visualItems = nullptr; - _settingsItem = nullptr; - _takeoffMissionItem = nullptr; - _updateContainsItems(); // This will clear containsItems which will be set again below. This will re-pop Start Mission confirmation. + _setupNewVisualItems(); - QmlObjectListModel* newControllerMissionItems = new QmlObjectListModel(this); const QList& newMissionItems = _missionManager->missionItems(); qCDebug(MissionControllerLog) << "loading from vehicle: count"<< newMissionItems.count(); - _missionItemCount = newMissionItems.count(); - emit missionItemCountChanged(_missionItemCount); - - MissionSettingsItem* settingsItem = _addMissionSettings(newControllerMissionItems); - int i=0; if (_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle() && newMissionItems.count() != 0) { // First item is fake home position MissionItem* fakeHomeItem = newMissionItems[0]; if (fakeHomeItem->coordinate().latitude() != 0 || fakeHomeItem->coordinate().longitude() != 0) { - settingsItem->setCoordinate(fakeHomeItem->coordinate()); + _settingsItem->setCoordinate(fakeHomeItem->coordinate()); } i = 1; } @@ -183,25 +171,24 @@ void MissionController::_newMissionItemsAvailableFromVehicle(bool removeAllReque SimpleMissionItem* simpleItem = new SimpleMissionItem(_masterController, _flyView, *missionItem); if (TakeoffMissionItem::isTakeoffCommand(static_cast(simpleItem->command()))) { // This needs to be a TakeoffMissionItem - _takeoffMissionItem = new TakeoffMissionItem(*missionItem, _masterController, _flyView, settingsItem, false /* forLoad */); + _takeoffMissionItem = new TakeoffMissionItem(*missionItem, _masterController, _flyView, _settingsItem, false /* forLoad */); _takeoffMissionItem->setWizardMode(false); simpleItem->deleteLater(); simpleItem = _takeoffMissionItem; } - newControllerMissionItems->append(simpleItem); + _visualItems->append(simpleItem); } - _visualItems = newControllerMissionItems; - _settingsItem = settingsItem; - // We set Altitude frame to mixed, otherwise if we need a non relative altitude frame we won't be able to change it setGlobalAltitudeFrame(weHaveItemsFromVehicle ? QGroundControlQmlGlobal::AltitudeFrameMixed : QGroundControlQmlGlobal::AltitudeFrameRelative); MissionController::_scanForAdditionalSettings(_visualItems, _masterController); _initAllVisualItems(); - _updateContainsItems(); + emit newItemsFromVehicle(); + + emit containsItemsChanged(); } _itemsRequested = false; } @@ -606,23 +593,46 @@ void MissionController::removeVisualItem(int viIndex) } } -void MissionController::removeAll(void) +void MissionController::_setupNewVisualItems(QmlObjectListModel* newItems) { - if (_visualItems) { + QmlObjectListModel* oldItems = _visualItems; + + if (oldItems) { _deinitAllVisualItems(); - _visualItems->clearAndDeleteContents(); - _visualItems->deleteLater(); - _settingsItem = nullptr; - _takeoffMissionItem = nullptr; + + // Destroy old items after a delay — TreeView delegates are torn down + // asynchronously during a polish cycle and may still hold bindings. + QTimer::singleShot(1000, oldItems, [oldItems] { + oldItems->clearAndDeleteContents(); + oldItems->deleteLater(); + }); + } + + _settingsItem = nullptr; + _takeoffMissionItem = nullptr; + + if (newItems) { + _visualItems = newItems; + if (_visualItems->count() == 0) { + _addMissionSettings(_visualItems); + } else { + _settingsItem = _visualItems->value(0); + } + } else { _visualItems = new QmlObjectListModel(this); _addMissionSettings(_visualItems); - _initAllVisualItems(); - setDirty(true); - _resetMissionFlightStatus(); - _allItemsRemoved(); } } +void MissionController::removeAll(void) +{ + _setupNewVisualItems(); + _initAllVisualItems(); + setDirty(true); + _resetMissionFlightStatus(); + _allItemsRemoved(); +} + bool MissionController::_loadJsonMissionFileV1(const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString) { // Validate root object keys @@ -993,20 +1003,7 @@ bool MissionController::_loadTextMissionFile(QTextStream& stream, QmlObjectListM void MissionController::_initLoadedVisualItems(QmlObjectListModel* loadedVisualItems) { - if (_visualItems) { - _deinitAllVisualItems(); - _visualItems->deleteLater(); - } - _settingsItem = nullptr; - _takeoffMissionItem = nullptr; - - _visualItems = loadedVisualItems; - - if (_visualItems->count() == 0) { - _addMissionSettings(_visualItems); - } else { - _settingsItem = _visualItems->value(0); - } + _setupNewVisualItems(loadedVisualItems); MissionController::_scanForAdditionalSettings(_visualItems, _masterController); @@ -2063,7 +2060,8 @@ void MissionController::_initAllVisualItems(void) _recalcAll(); connect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged); - connect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::_updateContainsItems); + connect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::containsItemsChanged); + connect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::missionItemCountChanged); // Connect for incremental tree model sync connect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted); @@ -2089,6 +2087,7 @@ void MissionController::_initAllVisualItems(void) emit visualItemsChanged(); emit containsItemsChanged(); emit plannedHomePositionChanged(plannedHomePosition()); + emit missionItemCountChanged(); if (!_flyView) { setCurrentPlanViewSeqNum(0, true); @@ -2099,13 +2098,7 @@ void MissionController::_initAllVisualItems(void) void MissionController::_deinitAllVisualItems(void) { - // Remove mission items from the tree model before their C++ objects are - // scheduled for deletion via deleteLater below. Without this, the TreeView - // delegates still hold references when the objects are destroyed, causing - // null-reference warnings in QML (e.g. "Cannot read property 'masterController' of null"). - _visualItemsTree.removeChildren(_missionGroupIndex); - - disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::_recalcAll); + disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::_recalcMissionFlightStatus); disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::plannedHomePositionChanged); for (int i=0; i<_visualItems->count(); i++) { @@ -2113,7 +2106,8 @@ void MissionController::_deinitAllVisualItems(void) } disconnect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged); - disconnect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::_updateContainsItems); + disconnect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::containsItemsChanged); + disconnect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::missionItemCountChanged); // Disconnect incremental tree model sync disconnect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted); @@ -2378,11 +2372,6 @@ void MissionController::_scanForAdditionalSettings(QmlObjectListModel* visualIte } } -void MissionController::_updateContainsItems(void) -{ - emit containsItemsChanged(); -} - bool MissionController::containsItems(void) const { return _visualItems ? _visualItems->count() > 1 : false; @@ -2472,7 +2461,7 @@ bool MissionController::showPlanFromManagerVehicle (void) qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: syncInProgress wait for signal"; return true; } else { - // Fake a _newMissionItemsAvailable with the current items + // Sync has already completed, fake a _newMissionItemsAvailable with the current items qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: sync complete simulate signal"; _itemsRequested = true; _newMissionItemsAvailableFromVehicle(false /* removeAllRequested */); diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 974ac5e15962..05bb53d506b4 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -296,7 +296,7 @@ class MissionController : public PlanElementController double minAMSLAltitude (void) const { return _minAMSLAltitude; } double maxAMSLAltitude (void) const { return _maxAMSLAltitude; } - int missionItemCount (void) const { return _missionItemCount; } + int missionItemCount (void) const { return _visualItems ? _visualItems->count() : 0; } int currentMissionIndex (void) const; int resumeMissionIndex (void) const; int currentPlanViewSeqNum (void) const { return _currentPlanViewSeqNum; } @@ -356,7 +356,7 @@ class MissionController : public PlanElementController void currentPlanViewItemChanged (void); void takeoffMissionItemChanged (void); void missionBoundingCubeChanged (void); - void missionItemCountChanged (int missionItemCount); + void missionItemCountChanged (void); void onlyInsertTakeoffValidChanged (void); void isInsertTakeoffValidChanged (void); void isInsertLandValidChanged (void); @@ -380,7 +380,6 @@ private slots: void _currentMissionIndexChanged (int sequenceNumber); void _recalcFlightPathSegments (void); void _recalcMissionFlightStatus (void); - void _updateContainsItems (void); void _progressPctChanged (double progressPct); void _visualItemsDirtyChanged (bool dirty); void _managerSendComplete (bool error); @@ -397,6 +396,7 @@ private slots: void _syncTreeRallyPointsInserted (const QModelIndex& parent, int first, int last); void _syncTreeRallyPointsAboutToBeRemoved (const QModelIndex& parent, int first, int last); void _syncTreeRallyPointsRemoved (const QModelIndex& parent, int first, int last); + void _syncTreeRallyPointsReset (void); private: void _init (void); @@ -405,6 +405,7 @@ private slots: void _recalcChildItems (void); void _recalcAllWithCoordinate (const QGeoCoordinate& coordinate); void _recalcROISpecialVisuals (void); + void _setupNewVisualItems (QmlObjectListModel* newItems = nullptr); void _initAllVisualItems (void); void _deinitAllVisualItems (void); void _initVisualItem (VisualMissionItem* item); @@ -445,7 +446,6 @@ private slots: Vehicle* _controllerVehicle = nullptr; Vehicle* _managerVehicle = nullptr; MissionManager* _missionManager = nullptr; - int _missionItemCount = 0; QmlObjectListModel* _visualItems = nullptr; QPersistentModelIndex _planFileGroupIndex; ///< Persistent index for "Plan File" group in tree QPersistentModelIndex _defaultsGroupIndex; ///< Persistent index for "Defaults" group in tree diff --git a/src/MissionManager/PlanMasterController.cc b/src/MissionManager/PlanMasterController.cc index 16fb3de8ac26..db4a8fbb2008 100644 --- a/src/MissionManager/PlanMasterController.cc +++ b/src/MissionManager/PlanMasterController.cc @@ -687,9 +687,9 @@ void PlanMasterController::sendPlanToVehicle(Vehicle* vehicle, const QString& fi void PlanMasterController::_showPlanFromManagerVehicle(void) { - if (!_managerVehicle->initialPlanRequestComplete() && !syncInProgress()) { - // Something went wrong with initial load. All controllers are idle, so just force it off - _managerVehicle->forceInitialPlanRequestComplete(); + if (!_managerVehicle->initialPlanRequestComplete()) { + // We need to wait until initial load is complete before we show anything. + return; } // The crazy if structure is to handle the load propagating by itself through the system From 2e294965e2a4d816e4158a11554bb5e681930f44 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 18 Mar 2026 18:04:11 -0700 Subject: [PATCH 113/119] MissionController: remove dead legacy .mission file loader code Remove methods and constants that were only used for the old standalone .mission JSON format (V1): - _loadJsonMissionFileV1(): V1 format loader with separate complexItems array - _loadJsonMissionFile(): dispatcher (declared but never implemented) - _loadItemsFromJson(): wrapper that was never called - _recalcROISpecialVisuals(): empty stub that was never effective - _jsonFileTypeValue, _jsonComplexItemsKey, _jsonMavAutopilotKey constants The current .plan format exclusively uses _loadJsonMissionFileV2() via PlanMasterController::loadFromFile() -> MissionController::load(), which remains unchanged. The .waypoints text import path is also unaffected. --- src/MissionManager/MissionController.cc | 150 ------------------------ src/MissionManager/MissionController.h | 9 -- 2 files changed, 159 deletions(-) diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 8c8ee0736708..75696c3b683a 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -399,7 +399,6 @@ VisualMissionItem* MissionController::insertROIMissionItem(QGeoCoordinate coordi simpleItem->setCommand(MAV_CMD_DO_SET_ROI) ; simpleItem->missionItem().setParam1(MAV_ROI_LOCATION); } - _recalcROISpecialVisuals(); return simpleItem; } @@ -411,7 +410,6 @@ VisualMissionItem* MissionController::insertCancelROIMissionItem(int visualItemI simpleItem->setCommand(MAV_CMD_DO_SET_ROI) ; simpleItem->missionItem().setParam1(MAV_ROI_NONE); } - _recalcROISpecialVisuals(); return simpleItem; } @@ -633,108 +631,6 @@ void MissionController::removeAll(void) _allItemsRemoved(); } -bool MissionController::_loadJsonMissionFileV1(const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString) -{ - // Validate root object keys - QList rootKeyInfoList = { - { _jsonPlannedHomePositionKey, QJsonValue::Object, true }, - { _jsonItemsKey, QJsonValue::Array, true }, - { _jsonMavAutopilotKey, QJsonValue::Double, false }, - { _jsonComplexItemsKey, QJsonValue::Array, true }, - }; - if (!JsonHelper::validateKeys(json, rootKeyInfoList, errorString)) { - return false; - } - - setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameMixed); - - // Read complex items - QList surveyItems; - QJsonArray complexArray(json[_jsonComplexItemsKey].toArray()); - qCDebug(MissionControllerLog) << "Json load: complex item count" << complexArray.count(); - for (int i=0; iload(itemObject, itemObject["id"].toInt(), errorString)) { - surveyItems.append(item); - } else { - return false; - } - } - - // Read simple items, interspersing complex items into the full list - - int nextSimpleItemIndex= 0; - int nextComplexItemIndex= 0; - int nextSequenceNumber = 1; // Start with 1 since home is in 0 - QJsonArray itemArray(json[_jsonItemsKey].toArray()); - - MissionSettingsItem* settingsItem = _addMissionSettings(visualItems); - if (json.contains(_jsonPlannedHomePositionKey)) { - SimpleMissionItem* item = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */); - if (item->load(json[_jsonPlannedHomePositionKey].toObject(), 0, errorString)) { - settingsItem->setCoordinate(item->coordinate()); - item->deleteLater(); - } else { - return false; - } - } - - qCDebug(MissionControllerLog) << "Json load: simple item loop start simpleItemCount:ComplexItemCount" << itemArray.count() << surveyItems.count(); - do { - qCDebug(MissionControllerLog) << "Json load: simple item loop nextSimpleItemIndex:nextComplexItemIndex:nextSequenceNumber" << nextSimpleItemIndex << nextComplexItemIndex << nextSequenceNumber; - - // If there is a complex item that should be next in sequence add it in - if (nextComplexItemIndex < surveyItems.count()) { - SurveyComplexItem* complexItem = surveyItems[nextComplexItemIndex]; - - if (complexItem->sequenceNumber() == nextSequenceNumber) { - qCDebug(MissionControllerLog) << "Json load: injecting complex item expectedSequence:actualSequence:" << nextSequenceNumber << complexItem->sequenceNumber(); - visualItems->append(complexItem); - nextSequenceNumber = complexItem->lastSequenceNumber() + 1; - nextComplexItemIndex++; - continue; - } - } - - // Add the next available simple item - if (nextSimpleItemIndex < itemArray.count()) { - const QJsonValue& itemValue = itemArray[nextSimpleItemIndex++]; - - if (!itemValue.isObject()) { - errorString = QStringLiteral("Mission item is not an object"); - return false; - } - - const QJsonObject itemObject = itemValue.toObject(); - SimpleMissionItem* item = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */); - if (item->load(itemObject, itemObject["id"].toInt(), errorString)) { - if (TakeoffMissionItem::isTakeoffCommand(item->mavCommand())) { - // This needs to be a TakeoffMissionItem - TakeoffMissionItem* takeoffItem = new TakeoffMissionItem(_masterController, _flyView, settingsItem, true /* forLoad */); - takeoffItem->load(itemObject, itemObject["id"].toInt(), errorString); - item->deleteLater(); - item = takeoffItem; - } - qCDebug(MissionControllerLog) << "Json load: adding simple item expectedSequence:actualSequence" << nextSequenceNumber << item->sequenceNumber(); - nextSequenceNumber = item->lastSequenceNumber() + 1; - visualItems->append(item); - } else { - return false; - } - } - } while (nextSimpleItemIndex < itemArray.count() || nextComplexItemIndex < surveyItems.count()); - - return true; -} - bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString) { // Validate root object keys @@ -926,19 +822,6 @@ bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjec return true; } -bool MissionController::_loadItemsFromJson(const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString) -{ - int fileVersion; - JsonHelper::validateExternalQGCJsonFile(json, - _jsonFileTypeValue, // expected file type - 2, // minimum supported version - 2, // maximum supported version - fileVersion, - errorString); - - return _loadJsonMissionFileV2(json, visualItems, errorString); -} - bool MissionController::_loadTextMissionFile(QTextStream& stream, QmlObjectListModel* visualItems, QString& errorString) { bool firstItem = true; @@ -1191,39 +1074,6 @@ FlightPathSegment* MissionController::_addFlightPathSegment(FlightPathSegmentHas return segment; } -void MissionController::_recalcROISpecialVisuals(void) -{ - return; - VisualMissionItem* lastCoordinateItem = qobject_cast(_visualItems->get(0)); - bool roiActive = false; - - for (int i=1; i<_visualItems->count(); i++) { - VisualMissionItem* visualItem = qobject_cast(_visualItems->get(i)); - SimpleMissionItem* simpleItem = qobject_cast(visualItem); - VisualItemPair viPair; - - if (simpleItem) { - if (roiActive) { - if (_isROICancelItem(simpleItem)) { - roiActive = false; - } - } else { - if (_isROIBeginItem(simpleItem)) { - roiActive = true; - } - } - } - - if (visualItem->specifiesCoordinate() && !visualItem->isStandaloneCoordinate()) { - viPair = VisualItemPair(lastCoordinateItem, visualItem); - if (_flightPathSegmentHashTable.contains(viPair)) { - _flightPathSegmentHashTable[viPair]->setSpecialVisual(roiActive); - } - lastCoordinateItem = visualItem; - } - } -} - void MissionController::_recalcFlightPathSegments(void) { VisualItemPair lastSegmentVisualItemPair; diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 05bb53d506b4..5138e55372c1 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -404,7 +404,6 @@ private slots: void _recalcSequence (void); void _recalcChildItems (void); void _recalcAllWithCoordinate (const QGeoCoordinate& coordinate); - void _recalcROISpecialVisuals (void); void _setupNewVisualItems (QmlObjectListModel* newItems = nullptr); void _initAllVisualItems (void); void _deinitAllVisualItems (void); @@ -414,8 +413,6 @@ private slots: void _calcPrevWaypointValues (VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference); bool _findPreviousAltitude (int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltitudeFrame* prevAltFrame); MissionSettingsItem* _addMissionSettings (QmlObjectListModel* visualItems); - bool _loadJsonMissionFile (const QByteArray& bytes, QmlObjectListModel* visualItems, QString& errorString); - bool _loadJsonMissionFileV1 (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); bool _loadJsonMissionFileV2 (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); bool _loadTextMissionFile (QTextStream& stream, QmlObjectListModel* visualItems, QString& errorString); int _nextSequenceNumber (void); @@ -425,7 +422,6 @@ private slots: void _addHoverTime (double hoverTime, double hoverDistance, int waypointIndex); void _addCruiseTime (double cruiseTime, double cruiseDistance, int wayPointIndex); void _updateBatteryInfo (int waypointIndex); - bool _loadItemsFromJson (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); void _initLoadedVisualItems (QmlObjectListModel* loadedVisualItems); FlightPathSegment* _addFlightPathSegment (FlightPathSegmentHashTable& prevItemPairHashTable, VisualItemPair& pair, bool mavlinkTerrainFrame); void _addTimeDistance (bool vtolInHover, double hoverTime, double cruiseTime, double extraTime, double distance, int seqNum); @@ -500,7 +496,6 @@ private slots: QGroundControlQmlGlobal::AltitudeFrame _globalAltFrame = QGroundControlQmlGlobal::AltitudeFrameRelative; static constexpr const char* _settingsGroup = "MissionController"; - static constexpr const char* _jsonFileTypeValue = "Mission"; static constexpr const char* _jsonItemsKey = "items"; static constexpr const char* _jsonPlannedHomePositionKey = "plannedHomePosition"; static constexpr const char* _jsonFirmwareTypeKey = "firmwareType"; @@ -510,9 +505,5 @@ private slots: static constexpr const char* _jsonParamsKey = "params"; static constexpr const char* _jsonGlobalPlanAltitudeModeKey = "globalPlanAltitudeMode"; - // Deprecated V1 format keys - static constexpr const char* _jsonComplexItemsKey = "complexItems"; - static constexpr const char* _jsonMavAutopilotKey = "MAV_AUTOPILOT"; - static constexpr int _missionFileVersion = 2; }; From 7e3b6cc4f046b45a17861aeb22b57673c903a985 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 18 Mar 2026 20:40:40 -0700 Subject: [PATCH 114/119] Extract MissionFlightStatusCalculator from MissionController --- src/MissionManager/CMakeLists.txt | 3 + src/MissionManager/MissionController.cc | 387 +---------------- src/MissionManager/MissionController.h | 37 +- src/MissionManager/MissionFlightStatus.h | 30 ++ .../MissionFlightStatusCalculator.cc | 393 ++++++++++++++++++ .../MissionFlightStatusCalculator.h | 52 +++ 6 files changed, 489 insertions(+), 413 deletions(-) create mode 100644 src/MissionManager/MissionFlightStatus.h create mode 100644 src/MissionManager/MissionFlightStatusCalculator.cc create mode 100644 src/MissionManager/MissionFlightStatusCalculator.h diff --git a/src/MissionManager/CMakeLists.txt b/src/MissionManager/CMakeLists.txt index 3a86a95c481b..5c2d25c40b94 100644 --- a/src/MissionManager/CMakeLists.txt +++ b/src/MissionManager/CMakeLists.txt @@ -37,6 +37,9 @@ target_sources(${CMAKE_PROJECT_NAME} MissionCommandUIInfo.h MissionController.cc MissionController.h + MissionFlightStatus.h + MissionFlightStatusCalculator.cc + MissionFlightStatusCalculator.h MissionItem.cc MissionItem.h MissionManager.cc diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 75696c3b683a..e0495ebca5ad 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -68,35 +68,8 @@ MissionController::~MissionController() void MissionController::_resetMissionFlightStatus(void) { - _missionFlightStatus.totalDistance = 0.0; - _missionFlightStatus.plannedDistance = 0.0; - _missionFlightStatus.maxTelemetryDistance = 0.0; - _missionFlightStatus.totalTime = 0.0; - _missionFlightStatus.hoverTime = 0.0; - _missionFlightStatus.cruiseTime = 0.0; - _missionFlightStatus.hoverDistance = 0.0; - _missionFlightStatus.cruiseDistance = 0.0; - _missionFlightStatus.cruiseSpeed = _controllerVehicle->defaultCruiseSpeed(); - _missionFlightStatus.hoverSpeed = _controllerVehicle->defaultHoverSpeed(); - _missionFlightStatus.vehicleSpeed = _controllerVehicle->multiRotor() || _managerVehicle->vtol() ? _missionFlightStatus.hoverSpeed : _missionFlightStatus.cruiseSpeed; - _missionFlightStatus.vehicleYaw = qQNaN(); - _missionFlightStatus.gimbalYaw = qQNaN(); - _missionFlightStatus.gimbalPitch = qQNaN(); - _missionFlightStatus.mAhBattery = 0; - _missionFlightStatus.hoverAmps = 0; - _missionFlightStatus.cruiseAmps = 0; - _missionFlightStatus.ampMinutesAvailable = 0; - _missionFlightStatus.hoverAmpsTotal = 0; - _missionFlightStatus.cruiseAmpsTotal = 0; - _missionFlightStatus.batteryChangePoint = -1; - _missionFlightStatus.batteriesRequired = -1; - _missionFlightStatus.vtolMode = _missionContainsVTOLTakeoff ? QGCMAVLink::VehicleClassMultiRotor : QGCMAVLink::VehicleClassFixedWing; - - _controllerVehicle->firmwarePlugin()->batteryConsumptionData(_controllerVehicle, _missionFlightStatus.mAhBattery, _missionFlightStatus.hoverAmps, _missionFlightStatus.cruiseAmps); - if (_missionFlightStatus.mAhBattery != 0) { - double batteryPercentRemainingAnnounce = SettingsManager::instance()->appSettings()->batteryPercentRemainingAnnounce()->rawValue().toDouble(); - _missionFlightStatus.ampMinutesAvailable = static_cast(_missionFlightStatus.mAhBattery) / 1000.0 * 60.0 * ((100.0 - batteryPercentRemainingAnnounce) / 100.0); - } + _flightStatusCalc.reset(_controllerVehicle, _managerVehicle, _missionContainsVTOLTakeoff); + _missionFlightStatus = _flightStatusCalc.status(); emit missionPlannedDistanceChanged(_missionFlightStatus.plannedDistance); emit missionTimeChanged(); @@ -107,7 +80,6 @@ void MissionController::_resetMissionFlightStatus(void) emit missionMaxTelemetryChanged(_missionFlightStatus.maxTelemetryDistance); emit batteryChangePointChanged(_missionFlightStatus.batteryChangePoint); emit batteriesRequiredChanged(_missionFlightStatus.batteriesRequired); - } void MissionController::start(bool flyView) @@ -993,29 +965,6 @@ void MissionController::save(QJsonObject& json) json[_jsonItemsKey] = rgJsonMissionItems; } -void MissionController::_calcPrevWaypointValues(VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference) -{ - QGeoCoordinate currentCoord = currentItem->entryCoordinate(); - QGeoCoordinate prevCoord = prevItem->exitCoordinate(); - - // Convert to fixed altitudes - - *altDifference = currentItem->amslEntryAlt() - prevItem->amslExitAlt(); - *distance = prevCoord.distanceTo(currentCoord); - *azimuth = prevCoord.azimuthTo(currentCoord); -} - -double MissionController::_calcDistanceToHome(VisualMissionItem* currentItem, VisualMissionItem* homeItem) -{ - QGeoCoordinate currentCoord = currentItem->entryCoordinate(); - QGeoCoordinate homeCoord = homeItem->exitCoordinate(); - bool distanceOk = false; - - distanceOk = true; - - return distanceOk ? homeCoord.distanceTo(currentCoord) : 0.0; -} - FlightPathSegment* MissionController::_createFlightPathSegmentWorker(VisualItemPair& pair, bool mavlinkTerrainFrame) { // The takeoff goes straight up from ground to alt and then over to specified position at same alt. Which means @@ -1261,317 +1210,18 @@ void MissionController::_recalcFlightPathSegments(void) } } -void MissionController::_updateBatteryInfo(int waypointIndex) -{ - if (_missionFlightStatus.mAhBattery != 0) { - _missionFlightStatus.hoverAmpsTotal = (_missionFlightStatus.hoverTime / 60.0) * _missionFlightStatus.hoverAmps; - _missionFlightStatus.cruiseAmpsTotal = (_missionFlightStatus.cruiseTime / 60.0) * _missionFlightStatus.cruiseAmps; - _missionFlightStatus.batteriesRequired = ceil((_missionFlightStatus.hoverAmpsTotal + _missionFlightStatus.cruiseAmpsTotal) / _missionFlightStatus.ampMinutesAvailable); - // FIXME: Battery change point code pretty much doesn't work. The reason is that is treats complex items as a black box. It needs to be able to look - // inside complex items in order to determine a swap point that is interior to a complex item. Current the swap point display in PlanToolbar is - // disabled to do this problem. - if (waypointIndex != -1 && _missionFlightStatus.batteriesRequired == 2 && _missionFlightStatus.batteryChangePoint == -1) { - _missionFlightStatus.batteryChangePoint = waypointIndex - 1; - } - } -} - -void MissionController::_addHoverTime(double hoverTime, double hoverDistance, int waypointIndex) -{ - _missionFlightStatus.totalTime += hoverTime; - _missionFlightStatus.hoverTime += hoverTime; - _missionFlightStatus.hoverDistance += hoverDistance; - _missionFlightStatus.plannedDistance += hoverDistance; - _updateBatteryInfo(waypointIndex); -} - -void MissionController::_addCruiseTime(double cruiseTime, double cruiseDistance, int waypointIndex) -{ - _missionFlightStatus.totalTime += cruiseTime; - _missionFlightStatus.cruiseTime += cruiseTime; - _missionFlightStatus.cruiseDistance += cruiseDistance; - _missionFlightStatus.plannedDistance += cruiseDistance; - _updateBatteryInfo(waypointIndex); -} - -/// Adds the specified time to the appropriate hover or cruise time values. -/// @param vtolInHover true: vtol is currrent in hover mode -/// @param hoverTime Amount of time tp add to hover -/// @param cruiseTime Amount of time to add to cruise -/// @param extraTime Amount of additional time to add to hover/cruise -/// @param seqNum Sequence number of waypoint for these values, -1 for no waypoint associated -void MissionController::_addTimeDistance(bool vtolInHover, double hoverTime, double cruiseTime, double extraTime, double distance, int seqNum) -{ - if (_controllerVehicle->vtol()) { - if (vtolInHover) { - _addHoverTime(hoverTime, distance, seqNum); - _addHoverTime(extraTime, 0, -1); - } else { - _addCruiseTime(cruiseTime, distance, seqNum); - _addCruiseTime(extraTime, 0, -1); - } - } else { - if (_controllerVehicle->multiRotor()) { - _addHoverTime(hoverTime, distance, seqNum); - _addHoverTime(extraTime, 0, -1); - } else { - _addCruiseTime(cruiseTime, distance, seqNum); - _addCruiseTime(extraTime, 0, -1); - } - } -} - void MissionController::_recalcMissionFlightStatus() { if (!_visualItems->count()) { return; } - bool firstCoordinateItem = true; - VisualMissionItem* lastFlyThroughVI = qobject_cast(_visualItems->get(0)); - - bool homePositionValid = _settingsItem->coordinate().isValid(); - qCDebug(MissionControllerLog) << "_recalcMissionFlightStatus"; - // If home position is valid we can calculate distances between all waypoints. - // If home position is not valid we can only calculate distances between waypoints which are - // both relative altitude. - - // No values for first item - lastFlyThroughVI->setAltDifference(0); - lastFlyThroughVI->setAzimuth(0); - lastFlyThroughVI->setDistance(0); - lastFlyThroughVI->setDistanceFromStart(0); - - _minAMSLAltitude = _maxAMSLAltitude = qQNaN(); - - _resetMissionFlightStatus(); - - bool linkStartToHome = false; - bool foundRTL = false; - bool pastLandCommand = false; - double totalHorizontalDistance = 0; - - for (int i=0; i<_visualItems->count(); i++) { - VisualMissionItem* item = qobject_cast(_visualItems->get(i)); - SimpleMissionItem* simpleItem = qobject_cast(item); - ComplexMissionItem* complexItem = qobject_cast(item); - - if (simpleItem && simpleItem->mavCommand() == MAV_CMD_NAV_RETURN_TO_LAUNCH) { - foundRTL = true; - } - - // Assume the worst - item->setAzimuth(0); - item->setDistance(0); - item->setDistanceFromStart(0); - - // Gimbal states reflect the state AFTER executing the item - - // ROI commands cancel out previous gimbal yaw/pitch - if (simpleItem) { - switch (simpleItem->command()) { - case MAV_CMD_NAV_ROI: - case MAV_CMD_DO_SET_ROI_LOCATION: - case MAV_CMD_DO_SET_ROI_WPNEXT_OFFSET: - case MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW: - _missionFlightStatus.gimbalYaw = qQNaN(); - _missionFlightStatus.gimbalPitch = qQNaN(); - break; - default: - break; - } - } - - // Look for specific gimbal changes - double gimbalYaw = item->specifiedGimbalYaw(); - if (!qIsNaN(gimbalYaw) || _planViewSettings->showGimbalOnlyWhenSet()->rawValue().toBool()) { - _missionFlightStatus.gimbalYaw = gimbalYaw; - } - double gimbalPitch = item->specifiedGimbalPitch(); - if (!qIsNaN(gimbalPitch) || _planViewSettings->showGimbalOnlyWhenSet()->rawValue().toBool()) { - _missionFlightStatus.gimbalPitch = gimbalPitch; - } - - // We don't need to do any more processing if: - // Mission Settings Item - // We are after an RTL command - if (i != 0 && !foundRTL) { - // We must set the mission flight status prior to querying for any values from the item. This is because things like - // current speed, gimbal, vtol state impact the values. - item->setMissionFlightStatus(_missionFlightStatus); - - // Link back to home if first item is takeoff and we have home position - if (firstCoordinateItem && simpleItem && (simpleItem->mavCommand() == MAV_CMD_NAV_TAKEOFF || simpleItem->mavCommand() == MAV_CMD_NAV_VTOL_TAKEOFF)) { - if (homePositionValid) { - linkStartToHome = true; - if (_controllerVehicle->multiRotor() || _controllerVehicle->vtol()) { - // We have to special case takeoff, assuming vehicle takes off straight up to specified altitude - double azimuth, distance, altDifference; - _calcPrevWaypointValues(_settingsItem, simpleItem, &azimuth, &distance, &altDifference); - double takeoffTime = qAbs(altDifference) / _appSettings->offlineEditingAscentSpeed()->rawValue().toDouble(); - _addHoverTime(takeoffTime, 0, -1); - } - } - } - - if (!pastLandCommand) - _addTimeDistance(_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor, 0, 0, item->additionalTimeDelay(), 0, -1); - - if (item->specifiesCoordinate()) { - - // Keep track of the min/max AMSL altitude for entire mission so we can calculate altitude percentages in terrain status display - if (simpleItem) { - double amslAltitude = item->amslEntryAlt(); - _minAMSLAltitude = std::fmin(_minAMSLAltitude, amslAltitude); - _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, amslAltitude); - } else { - // Complex item - double complexMinAMSLAltitude = complexItem->minAMSLAltitude(); - double complexMaxAMSLAltitude = complexItem->maxAMSLAltitude(); - _minAMSLAltitude = std::fmin(_minAMSLAltitude, complexMinAMSLAltitude); - _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, complexMaxAMSLAltitude); - } - - if (!item->isStandaloneCoordinate()) { - firstCoordinateItem = false; - - // Update vehicle yaw assuming direction to next waypoint and/or mission item change - if (simpleItem) { - double newVehicleYaw = simpleItem->specifiedVehicleYaw(); - if (qIsNaN(newVehicleYaw)) { - // No specific vehicle yaw set. Current vehicle yaw is determined from flight path segment direction. - if (simpleItem != lastFlyThroughVI) { - _missionFlightStatus.vehicleYaw = lastFlyThroughVI->exitCoordinate().azimuthTo(simpleItem->entryCoordinate()); - } - } else { - _missionFlightStatus.vehicleYaw = newVehicleYaw; - } - simpleItem->setMissionVehicleYaw(_missionFlightStatus.vehicleYaw); - } - - if (lastFlyThroughVI != _settingsItem || linkStartToHome) { - // This is a subsequent waypoint or we are forcing the first waypoint back to home - double azimuth, distance, altDifference; - - _calcPrevWaypointValues(item, lastFlyThroughVI, &azimuth, &distance, &altDifference); - - // If the last waypoint was a land command, there's a discontinuity at this point - if (!lastFlyThroughVI->isLandCommand()) { - totalHorizontalDistance += distance; - item->setDistance(distance); - - if (!pastLandCommand) { - // Calculate time/distance - double hoverTime = distance / _missionFlightStatus.hoverSpeed; - double cruiseTime = distance / _missionFlightStatus.cruiseSpeed; - _addTimeDistance(_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, 0, distance, item->sequenceNumber()); - } - } - - item->setAltDifference(altDifference); - item->setAzimuth(azimuth); - item->setDistanceFromStart(totalHorizontalDistance); - - _missionFlightStatus.maxTelemetryDistance = qMax(_missionFlightStatus.maxTelemetryDistance, _calcDistanceToHome(item, _settingsItem)); - } - - if (complexItem) { - // Add in distance/time inside complex items as well - double distance = complexItem->complexDistance(); - _missionFlightStatus.maxTelemetryDistance = qMax(_missionFlightStatus.maxTelemetryDistance, complexItem->greatestDistanceTo(complexItem->exitCoordinate())); - - if (!pastLandCommand) { - double hoverTime = distance / _missionFlightStatus.hoverSpeed; - double cruiseTime = distance / _missionFlightStatus.cruiseSpeed; - _addTimeDistance(_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, 0, distance, item->sequenceNumber()); - } - - totalHorizontalDistance += distance; - } - - - lastFlyThroughVI = item; - } - } - } - - // Speed, VTOL states changes are processed last since they take affect on the next item - - double newSpeed = item->specifiedFlightSpeed(); - if (!qIsNaN(newSpeed)) { - if (_controllerVehicle->multiRotor()) { - _missionFlightStatus.hoverSpeed = newSpeed; - } else if (_controllerVehicle->vtol()) { - if (_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor) { - _missionFlightStatus.hoverSpeed = newSpeed; - } else { - _missionFlightStatus.cruiseSpeed = newSpeed; - } - } else { - _missionFlightStatus.cruiseSpeed = newSpeed; - } - _missionFlightStatus.vehicleSpeed = newSpeed; - } - - // Update VTOL state - if (simpleItem && _controllerVehicle->vtol()) { - switch (simpleItem->command()) { - case MAV_CMD_NAV_TAKEOFF: // This will do a fixed wing style takeoff - case MAV_CMD_NAV_VTOL_TAKEOFF: // Vehicle goes straight up and then transitions to FW - case MAV_CMD_NAV_LAND: - _missionFlightStatus.vtolMode = QGCMAVLink::VehicleClassFixedWing; - break; - case MAV_CMD_NAV_VTOL_LAND: - _missionFlightStatus.vtolMode = QGCMAVLink::VehicleClassMultiRotor; - break; - case MAV_CMD_DO_VTOL_TRANSITION: - { - int transitionState = simpleItem->missionItem().param1(); - if (transitionState == MAV_VTOL_STATE_MC) { - _missionFlightStatus.vtolMode = QGCMAVLink::VehicleClassMultiRotor; - } else if (transitionState == MAV_VTOL_STATE_FW) { - _missionFlightStatus.vtolMode = QGCMAVLink::VehicleClassFixedWing; - } - } - break; - default: - break; - } - } - - if (item->isLandCommand()) { - pastLandCommand = true; - } - } - lastFlyThroughVI->setMissionVehicleYaw(_missionFlightStatus.vehicleYaw); - - // Add the information for the final segment back to home - if (foundRTL && lastFlyThroughVI != _settingsItem && homePositionValid) { - double azimuth, distance, altDifference; - _calcPrevWaypointValues(lastFlyThroughVI, _settingsItem, &azimuth, &distance, &altDifference); - - if (!pastLandCommand) { - // Calculate time/distance - double hoverTime = distance / _missionFlightStatus.hoverSpeed; - double cruiseTime = distance / _missionFlightStatus.cruiseSpeed; - double landTime = qAbs(altDifference) / _appSettings->offlineEditingDescentSpeed()->rawValue().toDouble(); - _addTimeDistance(_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, landTime, distance, -1); - } - } - - _missionFlightStatus.totalDistance = totalHorizontalDistance; - - if (_missionFlightStatus.mAhBattery != 0 && _missionFlightStatus.batteryChangePoint == -1) { - _missionFlightStatus.batteryChangePoint = 0; - } - - if (linkStartToHome) { - // Home position is taken into account for min/max values - _minAMSLAltitude = std::fmin(_minAMSLAltitude, _settingsItem->plannedHomePositionAltitude()->rawValue().toDouble()); - _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, _settingsItem->plannedHomePositionAltitude()->rawValue().toDouble()); - } + _flightStatusCalc.recalc(_visualItems, _settingsItem, _controllerVehicle, _managerVehicle, _appSettings, _planViewSettings, _missionContainsVTOLTakeoff); + _missionFlightStatus = _flightStatusCalc.status(); + _minAMSLAltitude = _flightStatusCalc.minAMSLAltitude(); + _maxAMSLAltitude = _flightStatusCalc.maxAMSLAltitude(); emit missionMaxTelemetryChanged (_missionFlightStatus.maxTelemetryDistance); emit missionTotalDistanceChanged (_missionFlightStatus.totalDistance); @@ -1586,31 +1236,6 @@ void MissionController::_recalcMissionFlightStatus() emit minAMSLAltitudeChanged (_minAMSLAltitude); emit maxAMSLAltitudeChanged (_maxAMSLAltitude); - // Walk the list again calculating altitude percentages - double altRange = _maxAMSLAltitude - _minAMSLAltitude; - for (int i=0; i<_visualItems->count(); i++) { - VisualMissionItem* item = qobject_cast(_visualItems->get(i)); - - if (item->specifiesCoordinate()) { - double amslAlt = item->amslEntryAlt(); - if (altRange == 0.0) { - item->setAltPercent(0.0); - item->setTerrainPercent(qQNaN()); - item->setTerrainCollision(false); - } else { - item->setAltPercent((amslAlt - _minAMSLAltitude) / altRange); - double terrainAltitude = item->terrainAltitude(); - if (qIsNaN(terrainAltitude)) { - item->setTerrainPercent(qQNaN()); - item->setTerrainCollision(false); - } else { - item->setTerrainPercent((terrainAltitude - _minAMSLAltitude) / altRange); - item->setTerrainCollision(amslAlt < terrainAltitude); - } - } - } - } - _updateTimer.start(UPDATE_TIMEOUT); emit recalcTerrainProfile(); diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 5138e55372c1..3c55ecd4932e 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -13,6 +13,8 @@ #include "QGCGeoBoundingCube.h" #include "QGroundControlQmlGlobal.h" #include "QGCMAVLink.h" +#include "MissionFlightStatus.h" +#include "MissionFlightStatusCalculator.h" Q_DECLARE_LOGGING_CATEGORY(MissionControllerLog) @@ -46,32 +48,8 @@ class MissionController : public PlanElementController MissionController(PlanMasterController* masterController, QObject* parent = nullptr); ~MissionController(); - typedef struct { - double maxTelemetryDistance; - double totalDistance; - double plannedDistance; - double totalTime; - double hoverDistance; - double hoverTime; - double cruiseDistance; - double cruiseTime; - int mAhBattery; ///< 0 for not available - double hoverAmps; ///< Amp consumption during hover - double cruiseAmps; ///< Amp consumption during cruise - double ampMinutesAvailable; ///< Amp minutes available from single battery - double hoverAmpsTotal; ///< Total hover amps used - double cruiseAmpsTotal; ///< Total cruise amps used - int batteryChangePoint; ///< -1 for not supported, 0 for not needed - int batteriesRequired; ///< -1 for not supported - double vehicleYaw; - double gimbalYaw; ///< NaN signals yaw was never changed - double gimbalPitch; ///< NaN signals pitch was never changed - // The following values are the state prior to executing this item - QGCMAVLink::VehicleClass_t vtolMode; ///< Either VehicleClassFixedWing, VehicleClassMultiRotor, VehicleClassGeneric (mode unknown) - double cruiseSpeed; - double hoverSpeed; - double vehicleSpeed; ///< Either cruise or hover speed based on vehicle type and vtol state - } MissionFlightStatus_t; + // Legacy alias kept for source compatibility with external code + using MissionFlightStatus_t = ::MissionFlightStatus_t; Q_PROPERTY(QmlObjectListModel* visualItems READ visualItems NOTIFY visualItemsChanged) Q_PROPERTY(QmlObjectTreeModel* visualItemsTree READ visualItemsTree CONSTANT) ///< Tree-structured view of visualItems for TreeView @@ -410,7 +388,6 @@ private slots: void _initVisualItem (VisualMissionItem* item); void _deinitVisualItem (VisualMissionItem* item); void _setupActiveVehicle (Vehicle* activeVehicle, bool forceLoadFromVehicle); - void _calcPrevWaypointValues (VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference); bool _findPreviousAltitude (int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltitudeFrame* prevAltFrame); MissionSettingsItem* _addMissionSettings (QmlObjectListModel* visualItems); bool _loadJsonMissionFileV2 (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); @@ -419,12 +396,8 @@ private slots: void _scanForAdditionalSettings (QmlObjectListModel* visualItems, PlanMasterController* masterController); void _setPlannedHomePositionFromFirstCoordinate(const QGeoCoordinate& clickCoordinate); void _resetMissionFlightStatus (void); - void _addHoverTime (double hoverTime, double hoverDistance, int waypointIndex); - void _addCruiseTime (double cruiseTime, double cruiseDistance, int wayPointIndex); - void _updateBatteryInfo (int waypointIndex); void _initLoadedVisualItems (QmlObjectListModel* loadedVisualItems); FlightPathSegment* _addFlightPathSegment (FlightPathSegmentHashTable& prevItemPairHashTable, VisualItemPair& pair, bool mavlinkTerrainFrame); - void _addTimeDistance (bool vtolInHover, double hoverTime, double cruiseTime, double extraTime, double distance, int seqNum); VisualMissionItem* _insertSimpleMissionItemWorker (QGeoCoordinate coordinate, MAV_CMD command, int visualItemIndex, bool makeCurrentItem); void _insertComplexMissionItemWorker (const QGeoCoordinate& mapCenterCoordinate, ComplexMissionItem* complexItem, int visualItemIndex, bool makeCurrentItem); bool _isROIBeginItem (SimpleMissionItem* simpleItem); @@ -433,7 +406,6 @@ private slots: void _allItemsRemoved (void); void _firstItemAdded (void); - static double _calcDistanceToHome (VisualMissionItem* currentItem, VisualMissionItem* homeItem); static double _normalizeLat (double lat); static double _normalizeLon (double lon); static bool _convertToMissionItems (QmlObjectListModel* visualMissionItems, QList& rgMissionItems, QObject* missionItemParent); @@ -469,6 +441,7 @@ private slots: bool _firstItemsFromVehicle = false; bool _itemsRequested = false; bool _inRecalcSequence = false; + MissionFlightStatusCalculator _flightStatusCalc; MissionFlightStatus_t _missionFlightStatus; AppSettings* _appSettings = nullptr; double _progressPct = 0; diff --git a/src/MissionManager/MissionFlightStatus.h b/src/MissionManager/MissionFlightStatus.h new file mode 100644 index 000000000000..09df427bbf2f --- /dev/null +++ b/src/MissionManager/MissionFlightStatus.h @@ -0,0 +1,30 @@ +#pragma once + +#include "QGCMAVLink.h" + +struct MissionFlightStatus_t { + double maxTelemetryDistance; + double totalDistance; + double plannedDistance; + double totalTime; + double hoverDistance; + double hoverTime; + double cruiseDistance; + double cruiseTime; + int mAhBattery; ///< 0 for not available + double hoverAmps; ///< Amp consumption during hover + double cruiseAmps; ///< Amp consumption during cruise + double ampMinutesAvailable; ///< Amp minutes available from single battery + double hoverAmpsTotal; ///< Total hover amps used + double cruiseAmpsTotal; ///< Total cruise amps used + int batteryChangePoint; ///< -1 for not supported, 0 for not needed + int batteriesRequired; ///< -1 for not supported + double vehicleYaw; + double gimbalYaw; ///< NaN signals yaw was never changed + double gimbalPitch; ///< NaN signals pitch was never changed + // The following values are the state prior to executing this item + QGCMAVLink::VehicleClass_t vtolMode; ///< Either VehicleClassFixedWing, VehicleClassMultiRotor, VehicleClassGeneric (mode unknown) + double cruiseSpeed; + double hoverSpeed; + double vehicleSpeed; ///< Either cruise or hover speed based on vehicle type and vtol state +}; diff --git a/src/MissionManager/MissionFlightStatusCalculator.cc b/src/MissionManager/MissionFlightStatusCalculator.cc new file mode 100644 index 000000000000..99ebedafff4c --- /dev/null +++ b/src/MissionManager/MissionFlightStatusCalculator.cc @@ -0,0 +1,393 @@ +#include "MissionFlightStatusCalculator.h" +#include "Vehicle.h" +#include "FirmwarePlugin.h" +#include "QmlObjectListModel.h" +#include "VisualMissionItem.h" +#include "SimpleMissionItem.h" +#include "ComplexMissionItem.h" +#include "MissionSettingsItem.h" +#include "AppSettings.h" +#include "PlanViewSettings.h" +#include "SettingsManager.h" + +#include + +void MissionFlightStatusCalculator::reset(Vehicle* controllerVehicle, Vehicle* managerVehicle, bool missionContainsVTOLTakeoff) +{ + _status.totalDistance = 0.0; + _status.plannedDistance = 0.0; + _status.maxTelemetryDistance = 0.0; + _status.totalTime = 0.0; + _status.hoverTime = 0.0; + _status.cruiseTime = 0.0; + _status.hoverDistance = 0.0; + _status.cruiseDistance = 0.0; + _status.cruiseSpeed = controllerVehicle->defaultCruiseSpeed(); + _status.hoverSpeed = controllerVehicle->defaultHoverSpeed(); + _status.vehicleSpeed = controllerVehicle->multiRotor() || managerVehicle->vtol() ? _status.hoverSpeed : _status.cruiseSpeed; + _status.vehicleYaw = qQNaN(); + _status.gimbalYaw = qQNaN(); + _status.gimbalPitch = qQNaN(); + _status.mAhBattery = 0; + _status.hoverAmps = 0; + _status.cruiseAmps = 0; + _status.ampMinutesAvailable = 0; + _status.hoverAmpsTotal = 0; + _status.cruiseAmpsTotal = 0; + _status.batteryChangePoint = -1; + _status.batteriesRequired = -1; + _status.vtolMode = missionContainsVTOLTakeoff ? QGCMAVLink::VehicleClassMultiRotor : QGCMAVLink::VehicleClassFixedWing; + + controllerVehicle->firmwarePlugin()->batteryConsumptionData(controllerVehicle, _status.mAhBattery, _status.hoverAmps, _status.cruiseAmps); + if (_status.mAhBattery != 0) { + double batteryPercentRemainingAnnounce = SettingsManager::instance()->appSettings()->batteryPercentRemainingAnnounce()->rawValue().toDouble(); + _status.ampMinutesAvailable = static_cast(_status.mAhBattery) / 1000.0 * 60.0 * ((100.0 - batteryPercentRemainingAnnounce) / 100.0); + } +} + +void MissionFlightStatusCalculator::recalc(QmlObjectListModel* visualItems, + MissionSettingsItem* settingsItem, + Vehicle* controllerVehicle, + Vehicle* managerVehicle, + AppSettings* appSettings, + PlanViewSettings* planViewSettings, + bool missionContainsVTOLTakeoff) +{ + bool firstCoordinateItem = true; + VisualMissionItem* lastFlyThroughVI = qobject_cast(visualItems->get(0)); + + bool homePositionValid = settingsItem->coordinate().isValid(); + + // If home position is valid we can calculate distances between all waypoints. + // If home position is not valid we can only calculate distances between waypoints which are + // both relative altitude. + + // No values for first item + lastFlyThroughVI->setAltDifference(0); + lastFlyThroughVI->setAzimuth(0); + lastFlyThroughVI->setDistance(0); + lastFlyThroughVI->setDistanceFromStart(0); + + _minAMSLAltitude = _maxAMSLAltitude = qQNaN(); + + reset(controllerVehicle, managerVehicle, missionContainsVTOLTakeoff); + + bool linkStartToHome = false; + bool foundRTL = false; + bool pastLandCommand = false; + double totalHorizontalDistance = 0; + + for (int i=0; icount(); i++) { + VisualMissionItem* item = qobject_cast(visualItems->get(i)); + SimpleMissionItem* simpleItem = qobject_cast(item); + ComplexMissionItem* complexItem = qobject_cast(item); + + if (simpleItem && simpleItem->mavCommand() == MAV_CMD_NAV_RETURN_TO_LAUNCH) { + foundRTL = true; + } + + // Assume the worst + item->setAzimuth(0); + item->setDistance(0); + item->setDistanceFromStart(0); + + // Gimbal states reflect the state AFTER executing the item + + // ROI commands cancel out previous gimbal yaw/pitch + if (simpleItem) { + switch (simpleItem->command()) { + case MAV_CMD_NAV_ROI: + case MAV_CMD_DO_SET_ROI_LOCATION: + case MAV_CMD_DO_SET_ROI_WPNEXT_OFFSET: + case MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW: + _status.gimbalYaw = qQNaN(); + _status.gimbalPitch = qQNaN(); + break; + default: + break; + } + } + + // Look for specific gimbal changes + double gimbalYaw = item->specifiedGimbalYaw(); + if (!qIsNaN(gimbalYaw) || planViewSettings->showGimbalOnlyWhenSet()->rawValue().toBool()) { + _status.gimbalYaw = gimbalYaw; + } + double gimbalPitch = item->specifiedGimbalPitch(); + if (!qIsNaN(gimbalPitch) || planViewSettings->showGimbalOnlyWhenSet()->rawValue().toBool()) { + _status.gimbalPitch = gimbalPitch; + } + + // We don't need to do any more processing if: + // Mission Settings Item + // We are after an RTL command + if (i != 0 && !foundRTL) { + // We must set the mission flight status prior to querying for any values from the item. This is because things like + // current speed, gimbal, vtol state impact the values. + item->setMissionFlightStatus(_status); + + // Link back to home if first item is takeoff and we have home position + if (firstCoordinateItem && simpleItem && (simpleItem->mavCommand() == MAV_CMD_NAV_TAKEOFF || simpleItem->mavCommand() == MAV_CMD_NAV_VTOL_TAKEOFF)) { + if (homePositionValid) { + linkStartToHome = true; + if (controllerVehicle->multiRotor() || controllerVehicle->vtol()) { + // We have to special case takeoff, assuming vehicle takes off straight up to specified altitude + double azimuth, distance, altDifference; + calcPrevWaypointValues(settingsItem, simpleItem, &azimuth, &distance, &altDifference); + double takeoffTime = qAbs(altDifference) / appSettings->offlineEditingAscentSpeed()->rawValue().toDouble(); + _addHoverTime(takeoffTime, 0, -1); + } + } + } + + if (!pastLandCommand) + _addTimeDistance(controllerVehicle, _status.vtolMode == QGCMAVLink::VehicleClassMultiRotor, 0, 0, item->additionalTimeDelay(), 0, -1); + + if (item->specifiesCoordinate()) { + + // Keep track of the min/max AMSL altitude for entire mission so we can calculate altitude percentages in terrain status display + if (simpleItem) { + double amslAltitude = item->amslEntryAlt(); + _minAMSLAltitude = std::fmin(_minAMSLAltitude, amslAltitude); + _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, amslAltitude); + } else { + // Complex item + double complexMinAMSLAltitude = complexItem->minAMSLAltitude(); + double complexMaxAMSLAltitude = complexItem->maxAMSLAltitude(); + _minAMSLAltitude = std::fmin(_minAMSLAltitude, complexMinAMSLAltitude); + _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, complexMaxAMSLAltitude); + } + + if (!item->isStandaloneCoordinate()) { + firstCoordinateItem = false; + + // Update vehicle yaw assuming direction to next waypoint and/or mission item change + if (simpleItem) { + double newVehicleYaw = simpleItem->specifiedVehicleYaw(); + if (qIsNaN(newVehicleYaw)) { + // No specific vehicle yaw set. Current vehicle yaw is determined from flight path segment direction. + if (simpleItem != lastFlyThroughVI) { + _status.vehicleYaw = lastFlyThroughVI->exitCoordinate().azimuthTo(simpleItem->entryCoordinate()); + } + } else { + _status.vehicleYaw = newVehicleYaw; + } + simpleItem->setMissionVehicleYaw(_status.vehicleYaw); + } + + if (lastFlyThroughVI != settingsItem || linkStartToHome) { + // This is a subsequent waypoint or we are forcing the first waypoint back to home + double azimuth, distance, altDifference; + + calcPrevWaypointValues(item, lastFlyThroughVI, &azimuth, &distance, &altDifference); + + // If the last waypoint was a land command, there's a discontinuity at this point + if (!lastFlyThroughVI->isLandCommand()) { + totalHorizontalDistance += distance; + item->setDistance(distance); + + if (!pastLandCommand) { + // Calculate time/distance + double hoverTime = distance / _status.hoverSpeed; + double cruiseTime = distance / _status.cruiseSpeed; + _addTimeDistance(controllerVehicle, _status.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, 0, distance, item->sequenceNumber()); + } + } + + item->setAltDifference(altDifference); + item->setAzimuth(azimuth); + item->setDistanceFromStart(totalHorizontalDistance); + + _status.maxTelemetryDistance = qMax(_status.maxTelemetryDistance, calcDistanceToHome(item, settingsItem)); + } + + if (complexItem) { + // Add in distance/time inside complex items as well + double distance = complexItem->complexDistance(); + _status.maxTelemetryDistance = qMax(_status.maxTelemetryDistance, complexItem->greatestDistanceTo(complexItem->exitCoordinate())); + + if (!pastLandCommand) { + double hoverTime = distance / _status.hoverSpeed; + double cruiseTime = distance / _status.cruiseSpeed; + _addTimeDistance(controllerVehicle, _status.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, 0, distance, item->sequenceNumber()); + } + + totalHorizontalDistance += distance; + } + + + lastFlyThroughVI = item; + } + } + } + + // Speed, VTOL states changes are processed last since they take affect on the next item + + double newSpeed = item->specifiedFlightSpeed(); + if (!qIsNaN(newSpeed)) { + if (controllerVehicle->multiRotor()) { + _status.hoverSpeed = newSpeed; + } else if (controllerVehicle->vtol()) { + if (_status.vtolMode == QGCMAVLink::VehicleClassMultiRotor) { + _status.hoverSpeed = newSpeed; + } else { + _status.cruiseSpeed = newSpeed; + } + } else { + _status.cruiseSpeed = newSpeed; + } + _status.vehicleSpeed = newSpeed; + } + + // Update VTOL state + if (simpleItem && controllerVehicle->vtol()) { + switch (simpleItem->command()) { + case MAV_CMD_NAV_TAKEOFF: // This will do a fixed wing style takeoff + case MAV_CMD_NAV_VTOL_TAKEOFF: // Vehicle goes straight up and then transitions to FW + case MAV_CMD_NAV_LAND: + _status.vtolMode = QGCMAVLink::VehicleClassFixedWing; + break; + case MAV_CMD_NAV_VTOL_LAND: + _status.vtolMode = QGCMAVLink::VehicleClassMultiRotor; + break; + case MAV_CMD_DO_VTOL_TRANSITION: + { + int transitionState = simpleItem->missionItem().param1(); + if (transitionState == MAV_VTOL_STATE_MC) { + _status.vtolMode = QGCMAVLink::VehicleClassMultiRotor; + } else if (transitionState == MAV_VTOL_STATE_FW) { + _status.vtolMode = QGCMAVLink::VehicleClassFixedWing; + } + } + break; + default: + break; + } + } + + if (item->isLandCommand()) { + pastLandCommand = true; + } + } + lastFlyThroughVI->setMissionVehicleYaw(_status.vehicleYaw); + + // Add the information for the final segment back to home + if (foundRTL && lastFlyThroughVI != settingsItem && homePositionValid) { + double azimuth, distance, altDifference; + calcPrevWaypointValues(lastFlyThroughVI, settingsItem, &azimuth, &distance, &altDifference); + + if (!pastLandCommand) { + // Calculate time/distance + double hoverTime = distance / _status.hoverSpeed; + double cruiseTime = distance / _status.cruiseSpeed; + double landTime = qAbs(altDifference) / appSettings->offlineEditingDescentSpeed()->rawValue().toDouble(); + _addTimeDistance(controllerVehicle, _status.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, landTime, distance, -1); + } + } + + _status.totalDistance = totalHorizontalDistance; + + if (_status.mAhBattery != 0 && _status.batteryChangePoint == -1) { + _status.batteryChangePoint = 0; + } + + if (linkStartToHome) { + // Home position is taken into account for min/max values + _minAMSLAltitude = std::fmin(_minAMSLAltitude, settingsItem->plannedHomePositionAltitude()->rawValue().toDouble()); + _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, settingsItem->plannedHomePositionAltitude()->rawValue().toDouble()); + } + + // Walk the list calculating altitude percentages + double altRange = _maxAMSLAltitude - _minAMSLAltitude; + for (int i=0; icount(); i++) { + VisualMissionItem* item = qobject_cast(visualItems->get(i)); + + if (item->specifiesCoordinate()) { + double amslAlt = item->amslEntryAlt(); + if (altRange == 0.0) { + item->setAltPercent(0.0); + item->setTerrainPercent(qQNaN()); + item->setTerrainCollision(false); + } else { + item->setAltPercent((amslAlt - _minAMSLAltitude) / altRange); + double terrainAltitude = item->terrainAltitude(); + if (qIsNaN(terrainAltitude)) { + item->setTerrainPercent(qQNaN()); + item->setTerrainCollision(false); + } else { + item->setTerrainPercent((terrainAltitude - _minAMSLAltitude) / altRange); + item->setTerrainCollision(amslAlt < terrainAltitude); + } + } + } + } +} + +void MissionFlightStatusCalculator::calcPrevWaypointValues(VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference) +{ + QGeoCoordinate currentCoord = currentItem->entryCoordinate(); + QGeoCoordinate prevCoord = prevItem->exitCoordinate(); + + *altDifference = currentItem->amslEntryAlt() - prevItem->amslExitAlt(); + *distance = prevCoord.distanceTo(currentCoord); + *azimuth = prevCoord.azimuthTo(currentCoord); +} + +double MissionFlightStatusCalculator::calcDistanceToHome(VisualMissionItem* currentItem, VisualMissionItem* homeItem) +{ + QGeoCoordinate currentCoord = currentItem->entryCoordinate(); + QGeoCoordinate homeCoord = homeItem->exitCoordinate(); + + return homeCoord.distanceTo(currentCoord); +} + +void MissionFlightStatusCalculator::_updateBatteryInfo(int waypointIndex) +{ + if (_status.mAhBattery != 0) { + _status.hoverAmpsTotal = (_status.hoverTime / 60.0) * _status.hoverAmps; + _status.cruiseAmpsTotal = (_status.cruiseTime / 60.0) * _status.cruiseAmps; + _status.batteriesRequired = ceil((_status.hoverAmpsTotal + _status.cruiseAmpsTotal) / _status.ampMinutesAvailable); + if (waypointIndex != -1 && _status.batteriesRequired == 2 && _status.batteryChangePoint == -1) { + _status.batteryChangePoint = waypointIndex - 1; + } + } +} + +void MissionFlightStatusCalculator::_addHoverTime(double hoverTime, double hoverDistance, int waypointIndex) +{ + _status.totalTime += hoverTime; + _status.hoverTime += hoverTime; + _status.hoverDistance += hoverDistance; + _status.plannedDistance += hoverDistance; + _updateBatteryInfo(waypointIndex); +} + +void MissionFlightStatusCalculator::_addCruiseTime(double cruiseTime, double cruiseDistance, int waypointIndex) +{ + _status.totalTime += cruiseTime; + _status.cruiseTime += cruiseTime; + _status.cruiseDistance += cruiseDistance; + _status.plannedDistance += cruiseDistance; + _updateBatteryInfo(waypointIndex); +} + +void MissionFlightStatusCalculator::_addTimeDistance(Vehicle* controllerVehicle, bool vtolInHover, double hoverTime, double cruiseTime, double extraTime, double distance, int seqNum) +{ + if (controllerVehicle->vtol()) { + if (vtolInHover) { + _addHoverTime(hoverTime, distance, seqNum); + _addHoverTime(extraTime, 0, -1); + } else { + _addCruiseTime(cruiseTime, distance, seqNum); + _addCruiseTime(extraTime, 0, -1); + } + } else { + if (controllerVehicle->multiRotor()) { + _addHoverTime(hoverTime, distance, seqNum); + _addHoverTime(extraTime, 0, -1); + } else { + _addCruiseTime(cruiseTime, distance, seqNum); + _addCruiseTime(extraTime, 0, -1); + } + } +} diff --git a/src/MissionManager/MissionFlightStatusCalculator.h b/src/MissionManager/MissionFlightStatusCalculator.h new file mode 100644 index 000000000000..603c19790d3c --- /dev/null +++ b/src/MissionManager/MissionFlightStatusCalculator.h @@ -0,0 +1,52 @@ +#pragma once + +#include "MissionFlightStatus.h" + +class AppSettings; +class ComplexMissionItem; +class MissionSettingsItem; +class PlanViewSettings; +class QmlObjectListModel; +class SimpleMissionItem; +class Vehicle; +class VisualMissionItem; + +/// Computes mission flight status (distances, times, battery, altitude range) +/// from a list of visual mission items and vehicle properties. +/// Extracted from MissionController to reduce its complexity. +class MissionFlightStatusCalculator +{ +public: + /// Resets the flight status fields to defaults based on vehicle properties. + void reset(Vehicle* controllerVehicle, Vehicle* managerVehicle, bool missionContainsVTOLTakeoff); + + /// Runs the full recalculation over all visual items, updating per-item + /// display properties and computing aggregate flight statistics. + void recalc(QmlObjectListModel* visualItems, + MissionSettingsItem* settingsItem, + Vehicle* controllerVehicle, + Vehicle* managerVehicle, + AppSettings* appSettings, + PlanViewSettings* planViewSettings, + bool missionContainsVTOLTakeoff); + + const MissionFlightStatus_t& status() const { return _status; } + double minAMSLAltitude() const { return _minAMSLAltitude; } + double maxAMSLAltitude() const { return _maxAMSLAltitude; } + + static void calcPrevWaypointValues(VisualMissionItem* currentItem, VisualMissionItem* prevItem, + double* azimuth, double* distance, double* altDifference); + static double calcDistanceToHome(VisualMissionItem* currentItem, VisualMissionItem* homeItem); + +private: + void _updateBatteryInfo(int waypointIndex); + void _addHoverTime(double hoverTime, double hoverDistance, int waypointIndex); + void _addCruiseTime(double cruiseTime, double cruiseDistance, int waypointIndex); + void _addTimeDistance(Vehicle* controllerVehicle, bool vtolInHover, + double hoverTime, double cruiseTime, double extraTime, + double distance, int seqNum); + + MissionFlightStatus_t _status {}; + double _minAMSLAltitude = 0; + double _maxAMSLAltitude = 0; +}; From f80ae3b6d34cfda4e731d8c5790b75cfe4324739 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 19 Mar 2026 09:21:03 -0700 Subject: [PATCH 115/119] MissionController: Collapse 10 plan-view signals into planViewStateChanged All 10 properties (currentPlanViewSeqNum, currentPlanViewVIIndex, currentPlanViewItem, previousCoordinate, onlyInsertTakeoffValid, isInsertTakeoffValid, isInsertLandValid, isROIActive, isROIBeginCurrentItem, flyThroughCommandsAllowed) are recomputed together in setCurrentPlanViewSeqNum, so a single NOTIFY signal suffices. QML bindings re-evaluate in one pass instead of 10 separate updates. splitSegmentChanged is kept separate because PlanView.qml has an explicit onSplitSegmentChanged handler. --- src/MissionManager/MissionController.cc | 15 ++++-------- src/MissionManager/MissionController.h | 31 +++++++++---------------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index e0495ebca5ad..73675d9f1baa 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -2142,17 +2142,12 @@ void MissionController::setCurrentPlanViewSeqNum(int sequenceNumber, bool force) _isInsertLandValid = _isInsertLandValid && !_onlyInsertTakeoffValid; _flyThroughCommandsAllowed = _flyThroughCommandsAllowed && !_onlyInsertTakeoffValid; - emit currentPlanViewSeqNumChanged(); - emit currentPlanViewVIIndexChanged(); - emit currentPlanViewItemChanged(); + // These 10 properties are all recomputed together above, so a single signal is sufficient. + // QML property bindings list planViewStateChanged as their NOTIFY signal which means + // one emit re-evaluates all dependent bindings in one pass instead of 10 separate updates. + // splitSegmentChanged is kept separate because PlanView.qml has an explicit onSplitSegmentChanged handler. + emit planViewStateChanged(); emit splitSegmentChanged(); - emit onlyInsertTakeoffValidChanged(); - emit isInsertTakeoffValidChanged(); - emit isInsertLandValidChanged(); - emit isROIActiveChanged(); - emit isROIBeginCurrentItemChanged(); - emit flyThroughCommandsAllowedChanged(); - emit previousCoordinateChanged(); } } diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 3c55ecd4932e..abb8130c3731 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -63,15 +63,15 @@ class MissionController : public PlanElementController Q_PROPERTY(QmlObjectListModel* directionArrows READ directionArrows CONSTANT) Q_PROPERTY(QStringList complexMissionItemNames READ complexMissionItemNames NOTIFY complexMissionItemNamesChanged) Q_PROPERTY(QGeoCoordinate plannedHomePosition READ plannedHomePosition NOTIFY plannedHomePositionChanged) ///< Includes AMSL altitude - Q_PROPERTY(QGeoCoordinate previousCoordinate MEMBER _previousCoordinate NOTIFY previousCoordinateChanged) + Q_PROPERTY(QGeoCoordinate previousCoordinate MEMBER _previousCoordinate NOTIFY planViewStateChanged) Q_PROPERTY(FlightPathSegment* splitSegment MEMBER _splitSegment NOTIFY splitSegmentChanged) ///< Segment which show show + split ui element Q_PROPERTY(double progressPct READ progressPct NOTIFY progressPctChanged) Q_PROPERTY(int missionItemCount READ missionItemCount NOTIFY missionItemCountChanged) ///< True mission item command count (only valid in Fly View) Q_PROPERTY(int currentMissionIndex READ currentMissionIndex NOTIFY currentMissionIndexChanged) Q_PROPERTY(int resumeMissionIndex READ resumeMissionIndex NOTIFY resumeMissionIndexChanged) ///< Returns the item index two which a mission should be resumed. -1 indicates resume mission not available. - Q_PROPERTY(int currentPlanViewSeqNum READ currentPlanViewSeqNum NOTIFY currentPlanViewSeqNumChanged) - Q_PROPERTY(int currentPlanViewVIIndex READ currentPlanViewVIIndex NOTIFY currentPlanViewVIIndexChanged) - Q_PROPERTY(VisualMissionItem* currentPlanViewItem READ currentPlanViewItem NOTIFY currentPlanViewItemChanged) + Q_PROPERTY(int currentPlanViewSeqNum READ currentPlanViewSeqNum NOTIFY planViewStateChanged) + Q_PROPERTY(int currentPlanViewVIIndex READ currentPlanViewVIIndex NOTIFY planViewStateChanged) + Q_PROPERTY(VisualMissionItem* currentPlanViewItem READ currentPlanViewItem NOTIFY planViewStateChanged) Q_PROPERTY(TakeoffMissionItem* takeoffMissionItem READ takeoffMissionItem NOTIFY takeoffMissionItemChanged) Q_PROPERTY(double missionTotalDistance READ missionTotalDistance NOTIFY missionTotalDistanceChanged) Q_PROPERTY(double missionPlannedDistance READ missionPlannedDistance NOTIFY missionPlannedDistanceChanged) @@ -87,14 +87,14 @@ class MissionController : public PlanElementController Q_PROPERTY(QString surveyComplexItemName READ surveyComplexItemName CONSTANT) Q_PROPERTY(QString corridorScanComplexItemName READ corridorScanComplexItemName CONSTANT) Q_PROPERTY(QString structureScanComplexItemName READ structureScanComplexItemName CONSTANT) - Q_PROPERTY(bool onlyInsertTakeoffValid MEMBER _onlyInsertTakeoffValid NOTIFY onlyInsertTakeoffValidChanged) - Q_PROPERTY(bool isInsertTakeoffValid MEMBER _isInsertTakeoffValid NOTIFY isInsertTakeoffValidChanged) - Q_PROPERTY(bool isInsertLandValid MEMBER _isInsertLandValid NOTIFY isInsertLandValidChanged) + Q_PROPERTY(bool onlyInsertTakeoffValid MEMBER _onlyInsertTakeoffValid NOTIFY planViewStateChanged) + Q_PROPERTY(bool isInsertTakeoffValid MEMBER _isInsertTakeoffValid NOTIFY planViewStateChanged) + Q_PROPERTY(bool isInsertLandValid MEMBER _isInsertLandValid NOTIFY planViewStateChanged) Q_PROPERTY(bool hasLandItem MEMBER _hasLandItem NOTIFY hasLandItemChanged) Q_PROPERTY(bool multipleLandPatternsAllowed READ multipleLandPatternsAllowed NOTIFY multipleLandPatternsAllowedChanged) - Q_PROPERTY(bool isROIActive MEMBER _isROIActive NOTIFY isROIActiveChanged) - Q_PROPERTY(bool isROIBeginCurrentItem MEMBER _isROIBeginCurrentItem NOTIFY isROIBeginCurrentItemChanged) - Q_PROPERTY(bool flyThroughCommandsAllowed MEMBER _flyThroughCommandsAllowed NOTIFY flyThroughCommandsAllowedChanged) + Q_PROPERTY(bool isROIActive MEMBER _isROIActive NOTIFY planViewStateChanged) + Q_PROPERTY(bool isROIBeginCurrentItem MEMBER _isROIBeginCurrentItem NOTIFY planViewStateChanged) + Q_PROPERTY(bool flyThroughCommandsAllowed MEMBER _flyThroughCommandsAllowed NOTIFY planViewStateChanged) Q_PROPERTY(double minAMSLAltitude MEMBER _minAMSLAltitude NOTIFY minAMSLAltitudeChanged) ///< Minimum altitude associated with this mission. Used to calculate percentages for terrain status. Q_PROPERTY(double maxAMSLAltitude MEMBER _maxAMSLAltitude NOTIFY maxAMSLAltitudeChanged) ///< Maximum altitude associated with this mission. Used to calculate percentages for terrain status. @@ -329,21 +329,12 @@ class MissionController : public PlanElementController void plannedHomePositionChanged (QGeoCoordinate plannedHomePosition); void progressPctChanged (double progressPct); void currentMissionIndexChanged (int currentMissionIndex); - void currentPlanViewSeqNumChanged (void); - void currentPlanViewVIIndexChanged (void); - void currentPlanViewItemChanged (void); + void planViewStateChanged (void); ///< All plan-view properties are recomputed together in setCurrentPlanViewSeqNum, so one signal covers them all void takeoffMissionItemChanged (void); void missionBoundingCubeChanged (void); void missionItemCountChanged (void); - void onlyInsertTakeoffValidChanged (void); - void isInsertTakeoffValidChanged (void); - void isInsertLandValidChanged (void); void hasLandItemChanged (void); void multipleLandPatternsAllowedChanged (void); - void isROIActiveChanged (void); - void isROIBeginCurrentItemChanged (void); - void flyThroughCommandsAllowedChanged (void); - void previousCoordinateChanged (void); void minAMSLAltitudeChanged (double minAMSLAltitude); void maxAMSLAltitudeChanged (double maxAMSLAltitude); void recalcTerrainProfile (void); From 3c44d4adbda44fb0474a2c0dca49f7b9ce55d41c Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sat, 14 Mar 2026 18:00:29 -0700 Subject: [PATCH 116/119] Vehicle: filter additional message handlers by component ID Same pattern as _handleHeartbeat and _handleSysStatus (PR #13768): ignore messages from non-autopilot components to prevent secondary MAVLink components from overriding core vehicle state. Handlers updated: - _handleExtendedSysState (flying/landing/VTOL state) - _handleGpsRawInt (GPS position and altitude) - _handleGlobalPositionInt (fused position and altitude) - _handleHomePosition (home/RTL location) - _handleCurrentMode (displayed flight mode) Relates-to: #13797 --- src/Vehicle/Vehicle.cc | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index f00c40099099..0804b06a07dd 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -772,6 +772,10 @@ void Vehicle::_handleCameraImageCaptured(const mavlink_message_t& message) // TODO: VehicleFactGroup void Vehicle::_handleGpsRawInt(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_gps_raw_int_t gpsRawInt; mavlink_msg_gps_raw_int_decode(&message, &gpsRawInt); @@ -794,6 +798,10 @@ void Vehicle::_handleGpsRawInt(mavlink_message_t& message) // TODO: VehicleFactGroup void Vehicle::_handleGlobalPositionInt(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_global_position_int_t globalPositionInt; mavlink_msg_global_position_int_decode(&message, &globalPositionInt); @@ -959,6 +967,10 @@ QString Vehicle::vehicleUIDStr() void Vehicle::_handleExtendedSysState(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_extended_sys_state_t extendedState; mavlink_msg_extended_sys_state_decode(&message, &extendedState); @@ -1124,6 +1136,10 @@ void Vehicle::_setHomePosition(QGeoCoordinate& homeCoord) void Vehicle::_handleHomePosition(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_home_position_t homePos; mavlink_msg_home_position_decode(&message, &homePos); @@ -1378,6 +1394,10 @@ void Vehicle::_handleHeartbeat(mavlink_message_t& message) void Vehicle::_handleCurrentMode(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_current_mode_t currentMode; mavlink_msg_current_mode_decode(&message, ¤tMode); if (currentMode.intended_custom_mode != 0) { // 0 == unknown/not supplied From 561feafd2a5aeb8ef921ecc95ef6ece455005557 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Thu, 19 Mar 2026 14:37:42 -0700 Subject: [PATCH 117/119] Add MAVLink 2 message signing support Implement MAVLink 2 message signing with named key management, per-vehicle auto-detection from incoming signed packets, and toolbar indicator. Key features: - Named signing key storage (SHA-256 hashed passphrases) via MAVLinkSigningKeys - Auto-detect signing key from incoming signed packets (no manual setup needed) - Send SETUP_SIGNING to enable/disable signing on vehicles - Toolbar indicator showing signing status with full key management UI - Shared SigningKeyManager QML component for settings and indicator - Track active key name per vehicle with proper signal emission Includes unit tests (SigningTest: 5 tests) and integration tests (MockLinkSigningTest: 3 tests) with QSignalSpy verification. Also renames docs/settings_view/mavlink.md to telemetry.md to match current UI naming. Fixes #14160 --- docs/assets/fly/toolbar/signing_indicator.jpg | Bin 0 -> 6166 bytes .../settings/mavlink/overview_telemetry.jpg | Bin 0 -> 265382 bytes docs/en/SUMMARY.md | 2 +- .../fly_view/fly_view_toolbar.md | 11 ++ docs/en/qgc-user-guide/plan_view/plan_view.md | 2 +- .../settings_view/settings_view.md | 2 +- .../{mavlink.md => telemetry.md} | 29 ++- .../troubleshooting/parameter_download.md | 4 +- src/Comms/LinkInterface.cc | 26 ++- src/Comms/LinkManager.cc | 14 -- src/Comms/LinkManager.h | 3 - src/Comms/MockLink/MockLink.cc | 25 +++ src/Comms/MockLink/MockLink.h | 4 + src/FirmwarePlugin/FirmwarePlugin.cc | 1 + src/MAVLink/CMakeLists.txt | 2 + src/MAVLink/MAVLinkSigning.cc | 130 ++++++++++++-- src/MAVLink/MAVLinkSigning.h | 22 ++- src/MAVLink/MAVLinkSigningKeys.cc | 170 ++++++++++++++++++ src/MAVLink/MAVLinkSigningKeys.h | 83 +++++++++ src/QmlControls/CMakeLists.txt | 1 + src/QmlControls/QGroundControlQmlGlobal.cc | 2 + src/QmlControls/QGroundControlQmlGlobal.h | 5 + src/QmlControls/SigningKeyManager.qml | 128 +++++++++++++ src/Settings/Mavlink.SettingsGroup.json | 6 - src/Settings/MavlinkSettings.cc | 15 -- src/Settings/MavlinkSettings.h | 4 - src/UI/AppSettings/TelemetrySettings.qml | 42 +---- src/UI/toolbar/CMakeLists.txt | 1 + src/UI/toolbar/SigningIndicator.qml | 81 +++++++++ src/Vehicle/Vehicle.cc | 77 +++++++- src/Vehicle/Vehicle.h | 7 +- test/CMakeLists.txt | 1 + test/MAVLink/CMakeLists.txt | 2 + test/MAVLink/MockLinkSigningTest.cc | 125 +++++++++++++ test/MAVLink/MockLinkSigningTest.h | 18 ++ test/MAVLink/SigningTest.cc | 123 +++++++++++-- test/MAVLink/SigningTest.h | 2 + 37 files changed, 1021 insertions(+), 149 deletions(-) create mode 100644 docs/assets/fly/toolbar/signing_indicator.jpg create mode 100644 docs/assets/settings/mavlink/overview_telemetry.jpg rename docs/en/qgc-user-guide/settings_view/{mavlink.md => telemetry.md} (71%) create mode 100644 src/MAVLink/MAVLinkSigningKeys.cc create mode 100644 src/MAVLink/MAVLinkSigningKeys.h create mode 100644 src/QmlControls/SigningKeyManager.qml create mode 100644 src/UI/toolbar/SigningIndicator.qml create mode 100644 test/MAVLink/MockLinkSigningTest.cc create mode 100644 test/MAVLink/MockLinkSigningTest.h diff --git a/docs/assets/fly/toolbar/signing_indicator.jpg b/docs/assets/fly/toolbar/signing_indicator.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c7f8dea46a4c7f3931e73312687e537b85ebfdd6 GIT binary patch literal 6166 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 literal 0 HcmV?d00001 diff --git a/docs/assets/settings/mavlink/overview_telemetry.jpg b/docs/assets/settings/mavlink/overview_telemetry.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9631a67f226ab4dd4839e31bf66b5d4d202a6080 GIT binary patch literal 265382 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 UfBeP0`9Dp|^zR+-z8^#X3m+Q?*8l(j literal 0 HcmV?d00001 diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index e5cc3d34955d..31e62fbaa89f 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -53,7 +53,7 @@ - [General](qgc-user-guide/settings_view/general.md) - [CSV Logging](qgc-user-guide/settings_view/csv.md) - [Offline Maps](qgc-user-guide/settings_view/offline_maps.md) - - [MAVLink](qgc-user-guide/settings_view/mavlink.md) + - [Telemetry](qgc-user-guide/settings_view/telemetry.md) - [Console Logging](qgc-user-guide/settings_view/console_logging.md) - [Virtual Joystick (PX4)](qgc-user-guide/settings_view/virtual_joystick.md) - [Analyze](qgc-user-guide/analyze_view/index.md) diff --git a/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md b/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md index 0adbc5390dba..3d6195ccd620 100644 --- a/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md +++ b/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md @@ -101,6 +101,17 @@ The Gimbal indicator is shown when the vehicle supports the [MAVLink Gimbal Prot For VTOL vehicles, a VTOL transition status indicator is shown when applicable. It indicates the current VTOL mode/state and provides transition-related status information. +### MAVLink Signing MAVLink Signing indicator + +The MAVLink Signing indicator appears when signing keys have been configured (see [MAVLink 2 Signing](../settings_view/telemetry.md#signing)). +It shows a lock icon that indicates whether MAVLink 2 message signing is active on the current vehicle connection: + +- **Locked (green):** Signing is active — the vehicle's incoming packets matched a stored key, or a key was manually enabled. +- **Unlocked:** Signing is not active on the current connection. + +The dropdown shows the signing status, the name of the active key (if any), and the number of saved keys. +Expanding the indicator provides full key management: you can enable a key on the vehicle, disable the active key, delete unused keys, or add new keys. + ### Multi-Vehicle Selector Multi-Vehicle indicator The Multi-Vehicle selector appears when more than one vehicle is connected. It allows you to quickly switch the active vehicle from the toolbar. diff --git a/docs/en/qgc-user-guide/plan_view/plan_view.md b/docs/en/qgc-user-guide/plan_view/plan_view.md index 06db598c7dcb..f5f85af8e7f5 100644 --- a/docs/en/qgc-user-guide/plan_view/plan_view.md +++ b/docs/en/qgc-user-guide/plan_view/plan_view.md @@ -185,7 +185,7 @@ If a failure occurs you should see a status message in the QGC UI similar to: > Mission transfer failed. Retry transfer. Error: Mission write mission count failed, maximum retries exceeded. -The loss rate for your link can be viewed in [Settings View > MAVLink](../settings_view/mavlink.md). +The loss rate for your link can be viewed in [App Settings > Telemetry](../settings_view/telemetry.md). The loss rate should be in the low single digits (i.e. maximum of 2 or 3): - A loss rate in the high single digits can lead to intermittent failures. diff --git a/docs/en/qgc-user-guide/settings_view/settings_view.md b/docs/en/qgc-user-guide/settings_view/settings_view.md index 78d3006666b3..b8b0c633fbe2 100644 --- a/docs/en/qgc-user-guide/settings_view/settings_view.md +++ b/docs/en/qgc-user-guide/settings_view/settings_view.md @@ -17,7 +17,7 @@ You can switch between the various settings options by clicking the buttons in t **[Offline Maps](offline_maps.md)**
    Allows you to cache maps for use while you have no Internet connection. -**[MAVLink](mavlink.md)** +**[Telemetry](telemetry.md)**
    Settings associated with the MAVLink connection to a vehicle. **[Console](console_logging.md)** diff --git a/docs/en/qgc-user-guide/settings_view/mavlink.md b/docs/en/qgc-user-guide/settings_view/telemetry.md similarity index 71% rename from docs/en/qgc-user-guide/settings_view/mavlink.md rename to docs/en/qgc-user-guide/settings_view/telemetry.md index 0ba6749f8327..758d9e752bb2 100644 --- a/docs/en/qgc-user-guide/settings_view/mavlink.md +++ b/docs/en/qgc-user-guide/settings_view/telemetry.md @@ -1,12 +1,10 @@ -# MAVLink Settings +# Telemetry Settings -The MAVLink settings (**SettingsView > MAVLink**) allow you to configure options and view information specific to MAVLink communications. +The Telemetry settings (**App Settings > Telemetry**) allow you to configure options and view information specific to MAVLink communications. This includes setting the MAVLink system ID for _QGroundControl_ and viewing link quality. The screen also allows you to manage [MAVLink 2 Log Streaming](#logging) (PX4 only), including _automating log upload to Flight Review_! -![MAVLink settings screen](../../../assets/settings/mavlink/overview.png) - ## Ground Station {#ground_station} This section sets the MAVLink properties and behaviour of _QGroundControl_. @@ -30,6 +28,29 @@ A high **Loss rate** may lead to protocol errors for things like parameter downl ![Link Status](../../../assets/settings/mavlink/link_status.jpg) +## MAVLink 2 Signing {#signing} + +The _MAVLink 2 Signing_ section (under **App Settings > Telemetry**) allows you to manage signing keys used for [MAVLink 2 message signing](https://mavlink.io/en/guide/message_signing.html). +When signing is enabled, all messages between QGroundControl and the vehicle are cryptographically authenticated, preventing unauthorized command injection. + +### Key Management + +- **Add Key:** Enter a friendly name and a passphrase. The passphrase is SHA-256 hashed to produce the 32-byte signing key. Only the hash is stored — the passphrase is not saved. +- **Enable:** Sends the signing key to the active vehicle via `SETUP_SIGNING`. The vehicle and QGroundControl will both begin signing messages. Only available when no key is currently active. +- **Disable:** Disables signing on the active vehicle by sending an all-zero key. Only shown for the currently active key. +- **Delete:** Removes a key from QGroundControl's key store. Not available for keys that are in use by any connected vehicle. A warning is shown because the vehicle may still have the key configured — deleting it locally means you can no longer communicate with that vehicle over a signed connection. + +### Auto-Detection + +When QGroundControl receives signed packets from a vehicle, it automatically tries each stored key to find a match. +If a match is found, signing is automatically configured on the link — no manual action is needed. +The detected key name is shown in the Signing toolbar indicator. + +::: warning +Signing keys should only be sent to the vehicle over secure (wired or encrypted) links. +Anyone who intercepts the key can sign messages and send commands to the vehicle. +::: + ## MAVLink 2 Logging (PX4 only) {#logging} The _MAVLink 2 Logging_ settings (PX4 only) configure real-time log streaming from PX4 to _QGroundControl_ and upload of logs to [Flight Review](https://logs.px4.io). diff --git a/docs/en/qgc-user-guide/troubleshooting/parameter_download.md b/docs/en/qgc-user-guide/troubleshooting/parameter_download.md index 547c9f1ae27e..f70d463b484c 100644 --- a/docs/en/qgc-user-guide/troubleshooting/parameter_download.md +++ b/docs/en/qgc-user-guide/troubleshooting/parameter_download.md @@ -7,9 +7,9 @@ At which point you will get an error stating that QGC was unable to retrieve the Although you can still fly the vehicle in this state it is not recommended. Also the vehicle setup pages will not be available. -You can see the loss rate for your link from the [Settings View > MAVLink](../settings_view/mavlink.md) page. +You can see the loss rate for your link from the [App Settings > Telemetry](../settings_view/telemetry.md) page. Even a loss rate in the high single digits can lead to intermittent failures of the plan protocols. -Higher loss rates could leads to 100% failure. +Higher loss rates could lead to 100% failure. There is also the more remote possibility of either firmware or QGC bugs. To see the details of the back and forth message traffic of the protocol you can turn on [Console Logging](../settings_view/console_logging.md) for the Parameter Protocol. diff --git a/src/Comms/LinkInterface.cc b/src/Comms/LinkInterface.cc index d2bcd60c3054..d967783dc1d0 100644 --- a/src/Comms/LinkInterface.cc +++ b/src/Comms/LinkInterface.cc @@ -3,8 +3,6 @@ #include "QGCApplication.h" #include "QGCLoggingCategory.h" #include "MAVLinkSigning.h" -#include "SettingsManager.h" -#include "MavlinkSettings.h" #include @@ -42,20 +40,16 @@ bool LinkInterface::mavlinkChannelIsSet() const bool LinkInterface::initMavlinkSigning() { - if (!isSecureConnection()) { - auto mavlinkSettings = SettingsManager::instance()->mavlinkSettings(); - const QByteArray signingKeyBytes = mavlinkSettings->mavlink2SigningKey()->rawValue().toByteArray(); - if (MAVLinkSigning::initSigning(static_cast(_mavlinkChannel), signingKeyBytes, MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)) { - if (signingKeyBytes.isEmpty()) { - qCDebug(LinkInterfaceLog) << "Signing disabled on channel" << _mavlinkChannel; - } else { - qCDebug(LinkInterfaceLog) << "Signing enabled on channel" << _mavlinkChannel; - } - } else { - qCWarning(LinkInterfaceLog) << "Failed To enable Signing on channel" << _mavlinkChannel; - // FIXME: What should we do here? - return false; - } + // Always clear any prior signing state on the channel to avoid stale + // mavlink_status_t::signing from a previous connection on this channel. + // For insecure connections the correct key will be auto-detected from + // incoming signed packets via MAVLinkSigning::tryDetectKey(). + if (MAVLinkSigning::initSigning(static_cast(_mavlinkChannel), QByteArrayView(), nullptr)) { + qCDebug(LinkInterfaceLog) << "Signing cleared on channel" << _mavlinkChannel + << (isSecureConnection() ? "(secure)" : "(will auto-detect)"); + } else { + qCWarning(LinkInterfaceLog) << "Failed to initialise signing on channel" << _mavlinkChannel; + return false; } return true; diff --git a/src/Comms/LinkManager.cc b/src/Comms/LinkManager.cc index 5c9aa6d9b08e..cd127a8ed76e 100644 --- a/src/Comms/LinkManager.cc +++ b/src/Comms/LinkManager.cc @@ -785,20 +785,6 @@ bool LinkManager::isLinkUSBDirect(const LinkInterface *link) return false; } -void LinkManager::resetMavlinkSigning() -{ - // Make a copy under mutex protection to avoid holding lock during signing initialization - QList links; - { - QMutexLocker locker(&_linksMutex); - links = _rgLinks; - } - - for (const SharedLinkInterfacePtr &sharedLink: links) { - sharedLink->initMavlinkSigning(); - } -} - #ifndef QGC_NO_SERIAL_LINK // Serial Only Functions void LinkManager::_filterCompositePorts(QList &portList) diff --git a/src/Comms/LinkManager.h b/src/Comms/LinkManager.h index 964b1ecfeb7d..69fa18eaadf8 100644 --- a/src/Comms/LinkManager.h +++ b/src/Comms/LinkManager.h @@ -88,9 +88,6 @@ class LinkManager : public QObject /// Returns pointer to the mavlink support forwarding link, or nullptr if it does not exist SharedLinkInterfacePtr mavlinkForwardingSupportLink(); - /// Re-initilize the mavlink signing for all links. Used when the signing key changes. - void resetMavlinkSigning(); - void disconnectAll(); /// Allocates a mavlink channel for use diff --git a/src/Comms/MockLink/MockLink.cc b/src/Comms/MockLink/MockLink.cc index 90b352fd6368..bf5b07477766 100644 --- a/src/Comms/MockLink/MockLink.cc +++ b/src/Comms/MockLink/MockLink.cc @@ -699,6 +699,9 @@ void MockLink::_handleIncomingMavlinkMsg(const mavlink_message_t &msg) case MAVLINK_MSG_ID_PARAM_MAP_RC: _handleParamMapRC(msg); break; + case MAVLINK_MSG_ID_SETUP_SIGNING: + _handleSetupSigning(msg); + break; default: break; } @@ -726,6 +729,28 @@ void MockLink::_handleParamMapRC(const mavlink_message_t &msg) } } +void MockLink::_handleSetupSigning(const mavlink_message_t &msg) +{ + mavlink_setup_signing_t setupSigning{}; + mavlink_msg_setup_signing_decode(&msg, &setupSigning); + + if (setupSigning.target_system != _vehicleSystemId) { + return; + } + + // All-zero key = disable signing + bool allZeroKey = true; + for (const uint8_t byte : setupSigning.secret_key) { + if (byte != 0) { + allZeroKey = false; + break; + } + } + + _signingEnabled = !allZeroKey; + qCDebug(MockLinkLog) << "Signing" << (_signingEnabled ? "enabled" : "disabled"); +} + void MockLink::_handleSetMode(const mavlink_message_t &msg) { mavlink_set_mode_t request{}; diff --git a/src/Comms/MockLink/MockLink.h b/src/Comms/MockLink/MockLink.h index e42d383bf06a..52d5259d0021 100644 --- a/src/Comms/MockLink/MockLink.h +++ b/src/Comms/MockLink/MockLink.h @@ -52,6 +52,8 @@ class MockLink : public LinkInterface double vehicleLongitude() const { return _vehicleLongitude; } double vehicleAltitudeAMSL() const { return _vehicleAltitudeAMSL; } + bool signingEnabled() const { return _signingEnabled; } + /// Sends the specified mavlink message to QGC void respondWithMavlinkMessage(const mavlink_message_t &msg); @@ -208,6 +210,7 @@ private slots: void _handleLogRequestList(const mavlink_message_t &msg); void _handleLogRequestData(const mavlink_message_t &msg); void _handleParamMapRC(const mavlink_message_t &msg); + void _handleSetupSigning(const mavlink_message_t &msg); void _handleRequestMessage(const mavlink_command_long_t &request, bool &accepted, bool &noAck); void _handleRequestMessageAutopilotVersion(const mavlink_command_long_t &request, bool &accepted); void _handleRequestMessageDebug(const mavlink_command_long_t &request, bool &accepted, bool &noAck); @@ -289,6 +292,7 @@ private slots: double _vehicleAltitudeAMSL = _defaultVehicleHomeAltitude; bool _commLost = false; + bool _signingEnabled = false; bool _highLatencyTransmissionEnabled = true; int _sendHomePositionDelayCount = 10; ///< No home position for 4 seconds diff --git a/src/FirmwarePlugin/FirmwarePlugin.cc b/src/FirmwarePlugin/FirmwarePlugin.cc index c081abc57375..e0f9cd78ca5d 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.cc +++ b/src/FirmwarePlugin/FirmwarePlugin.cc @@ -204,6 +204,7 @@ const QVariantList &FirmwarePlugin::toolIndicators(const Vehicle*) QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/GimbalIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/EscIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/JoystickIndicator.qml")), + QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/SigningIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/MultiVehicleSelector.qml")), #ifdef QT_DEBUG // ControlIndicator is only available in debug builds for the moment diff --git a/src/MAVLink/CMakeLists.txt b/src/MAVLink/CMakeLists.txt index c31b7b55196d..5bf39bd94f07 100644 --- a/src/MAVLink/CMakeLists.txt +++ b/src/MAVLink/CMakeLists.txt @@ -12,6 +12,8 @@ target_sources(${CMAKE_PROJECT_NAME} MAVLinkLib.h MAVLinkSigning.cc MAVLinkSigning.h + MAVLinkSigningKeys.cc + MAVLinkSigningKeys.h MAVLinkStreamConfig.cc MAVLinkStreamConfig.h QGCMAVLink.cc diff --git a/src/MAVLink/MAVLinkSigning.cc b/src/MAVLink/MAVLinkSigning.cc index d9cd8794f8aa..b420ab8b2d9c 100644 --- a/src/MAVLink/MAVLinkSigning.cc +++ b/src/MAVLink/MAVLinkSigning.cc @@ -1,12 +1,18 @@ #include "MAVLinkSigning.h" +#include "MAVLinkSigningKeys.h" #include "QGCMAVLink.h" -#include "DeviceInfo.h" +#include "QmlObjectListModel.h" +#include #include namespace { +// Per-channel cache of the last key name that successfully matched. +// Empty means no cached hint. Avoids O(n) SHA-256 on every incoming signed packet. +static QString s_channelKeyHint[MAVLINK_COMM_NUM_BUFFERS]; + mavlink_signing_t* _getChannelSigning(uint8_t channel) { mavlink_status_t* const status = mavlink_get_channel_status(channel); @@ -22,16 +28,10 @@ mavlink_channel_t _getMessageChannel(const mavlink_message_t &message) return static_cast(message.signature[0]); } -void _setSigningKey(mavlink_signing_t *signing, QByteArrayView key, bool randomize = false) +void _setSigningKey(mavlink_signing_t *signing, QByteArrayView key) { - if (randomize) { - const size_t key_size = sizeof(signing->secret_key) / 4; - uint32_t secret_key[key_size]; - QRandomGenerator::global()->fillRange(secret_key, key_size); - (void) memcpy(signing->secret_key, secret_key, sizeof(signing->secret_key)); - } else if (!key.isEmpty()) { - const QByteArray hash = QCryptographicHash::hash(key, QCryptographicHash::Sha256); - (void) memcpy(signing->secret_key, hash.constData(), sizeof(signing->secret_key)); + if (!key.isEmpty() && key.size() >= static_cast(sizeof(signing->secret_key))) { + (void) memcpy(signing->secret_key, key.constData(), sizeof(signing->secret_key)); } else { (void) memset(signing->secret_key, 0, sizeof(signing->secret_key)); } @@ -50,7 +50,7 @@ void _setSigningTimestamp(mavlink_signing_t *signing) namespace MAVLinkSigning { -bool secureConnectionAccceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id) +bool secureConnectionAcceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id) { Q_UNUSED(status); Q_UNUSED(message_id); @@ -58,7 +58,7 @@ bool secureConnectionAccceptUnsignedCallback(const mavlink_status_t *status, uin return true; } -bool insecureConnectionAccceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id) +bool insecureConnectionAcceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id) { Q_UNUSED(status); @@ -85,6 +85,7 @@ bool initSigning(mavlink_channel_t channel, QByteArrayView key, mavlink_accept_u if (key.isEmpty()) { status->signing = nullptr; status->signing_streams = nullptr; + s_channelKeyHint[channel].clear(); } else { static mavlink_signing_t s_signing[MAVLINK_COMM_NUM_BUFFERS]; static mavlink_signing_streams_t s_signing_streams; @@ -115,19 +116,112 @@ bool checkSigningLinkId(mavlink_channel_t channel, const mavlink_message_t &mess return (signing->link_id == _getMessageChannel(message)); } -/// Create a setup signing message for a target system. -/// Assumes that signing has already been initialized for the channel. -void createSetupSigning(mavlink_channel_t channel, mavlink_system_t target_system, mavlink_setup_signing_t &setup_signing) +void createSetupSigning(mavlink_channel_t channel, mavlink_system_t target_system, QByteArrayView keyBytes, mavlink_setup_signing_t &setup_signing) { (void) memset(&setup_signing, 0, sizeof(setup_signing)); setup_signing.target_system = target_system.sysid; setup_signing.target_component = target_system.compid; + if (!keyBytes.isEmpty() && keyBytes.size() >= static_cast(sizeof(setup_signing.secret_key))) { + const mavlink_signing_t* const signing = _getChannelSigning(channel); + if (signing) { + setup_signing.initial_timestamp = signing->timestamp; + } else { + static const QDateTime offset_time = QDateTime(QDate(2015, 1, 1).startOfDay()); + setup_signing.initial_timestamp = static_cast(offset_time.msecsTo(QDateTime::currentDateTimeUtc())) * 100; + } + (void) memcpy(setup_signing.secret_key, keyBytes.constData(), sizeof(setup_signing.secret_key)); + } +} + +bool isSigningEnabled(mavlink_channel_t channel) +{ const mavlink_signing_t* const signing = _getChannelSigning(channel); - if (signing) { - setup_signing.initial_timestamp = signing->timestamp; - (void) memcpy(setup_signing.secret_key, signing->secret_key, sizeof(setup_signing.secret_key)); + return (signing != nullptr); +} + +void createDisableSigning(mavlink_system_t target_system, mavlink_setup_signing_t &setup_signing) +{ + (void) memset(&setup_signing, 0, sizeof(setup_signing)); + setup_signing.target_system = target_system.sysid; + setup_signing.target_component = target_system.compid; +} + +bool isMessageSigned(const mavlink_message_t &message) +{ + return (message.incompat_flags & MAVLINK_IFLAG_SIGNED) != 0; +} + +bool verifySignature(QByteArrayView key, const mavlink_message_t &message) +{ + if (key.size() < 32) { + return false; + } + + // Replicate the C library's signature computation: + // SHA-256(secret_key + header_bytes + payload + CRC + link_id + timestamp) + // Then compare first 6 bytes with message.signature[7..12] + + const uint8_t* header = reinterpret_cast(&message.magic); + const char* payload = reinterpret_cast(&message.payload64[0]); + const uint8_t* crc = reinterpret_cast(payload + message.len); + const uint8_t* sig = message.signature; // [link_id(1), timestamp(6), hash(6)] + + QCryptographicHash sha256(QCryptographicHash::Sha256); + sha256.addData(key.first(32)); + sha256.addData(QByteArrayView(reinterpret_cast(header), MAVLINK_NUM_HEADER_BYTES)); + sha256.addData(QByteArrayView(payload, message.len)); + sha256.addData(QByteArrayView(reinterpret_cast(crc), 2)); + sha256.addData(QByteArrayView(reinterpret_cast(sig), 7)); // link_id + timestamp + + const QByteArray hash = sha256.result(); + return (memcmp(hash.constData(), sig + 7, 6) == 0); +} + +QString tryDetectKey(mavlink_channel_t channel, const mavlink_message_t &message) +{ + if (!isMessageSigned(message)) { + return QString(); + } + + // If signing is already configured on this channel, the C library already verified it + if (isSigningEnabled(channel)) { + return QString(); + } + + auto* signingKeys = MAVLinkSigningKeys::instance(); + const auto* keys = signingKeys->keys(); + const int keyCount = keys->count(); + + // Try the cached hint for this channel first + const QString& hintName = s_channelKeyHint[channel]; + if (!hintName.isEmpty()) { + const QByteArray keyBytes = signingKeys->keyBytesByName(hintName); + if (!keyBytes.isEmpty() && verifySignature(keyBytes, message)) { + if (initSigning(channel, keyBytes, insecureConnectionAcceptUnsignedCallback)) { + qCInfo(QGCMAVLinkLog) << "Auto-detected signing key" << hintName << "on channel" << channel << "(cached hint)"; + return hintName; + } + } + } + + // Fall back to trying all keys + for (int i = 0; i < keyCount; ++i) { + const QString name = signingKeys->keyNameAt(i); + if (name == hintName) { + continue; // already tried above + } + const QByteArray keyBytes = signingKeys->keyBytesAt(i); + if (verifySignature(keyBytes, message)) { + if (initSigning(channel, keyBytes, insecureConnectionAcceptUnsignedCallback)) { + s_channelKeyHint[channel] = name; + qCInfo(QGCMAVLinkLog) << "Auto-detected signing key" << name << "on channel" << channel; + return name; + } + } } + + return QString(); } } // namespace MAVLinkSigning diff --git a/src/MAVLink/MAVLinkSigning.h b/src/MAVLink/MAVLinkSigning.h index 4f5947138d35..75b161c5e076 100644 --- a/src/MAVLink/MAVLinkSigning.h +++ b/src/MAVLink/MAVLinkSigning.h @@ -1,15 +1,31 @@ #pragma once #include +#include #include #include "MAVLinkLib.h" namespace MAVLinkSigning { - bool secureConnectionAccceptUnsignedCallback(const mavlink_status_t *s0tatus, uint32_t message_id); - bool insecureConnectionAccceptUnsignedCallback(const mavlink_status_t *s0tatus, uint32_t message_id); + bool secureConnectionAcceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id); + bool insecureConnectionAcceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id); bool initSigning(mavlink_channel_t channel, QByteArrayView key, mavlink_accept_unsigned_t callback); bool checkSigningLinkId(mavlink_channel_t channel, const mavlink_message_t &message); - void createSetupSigning(mavlink_channel_t channel, mavlink_system_t target_system, mavlink_setup_signing_t &setup_signing); + bool isSigningEnabled(mavlink_channel_t channel); + + /// Create a SETUP_SIGNING payload using the given key bytes directly. + void createSetupSigning(mavlink_channel_t channel, mavlink_system_t target_system, QByteArrayView keyBytes, mavlink_setup_signing_t &setup_signing); + void createDisableSigning(mavlink_system_t target_system, mavlink_setup_signing_t &setup_signing); + + /// Returns true if the message has a MAVLink2 signature. + bool isMessageSigned(const mavlink_message_t &message); + + /// Verify a key against a signed message's signature. + bool verifySignature(QByteArrayView key, const mavlink_message_t &message); + + /// Try all stored signing keys against a signed message. If a match is found, + /// configure signing on the channel with that key. + /// Returns the matching key name, or empty string if no match. + QString tryDetectKey(mavlink_channel_t channel, const mavlink_message_t &message); }; // namespace MAVLinkSigning diff --git a/src/MAVLink/MAVLinkSigningKeys.cc b/src/MAVLink/MAVLinkSigningKeys.cc new file mode 100644 index 000000000000..a33a20f2946c --- /dev/null +++ b/src/MAVLink/MAVLinkSigningKeys.cc @@ -0,0 +1,170 @@ +#include "MAVLinkSigningKeys.h" +#include "QGCLoggingCategory.h" +#include "QmlObjectListModel.h" +#include "MultiVehicleManager.h" +#include "Vehicle.h" + +#include +#include + +QGC_LOGGING_CATEGORY(MAVLinkSigningKeysLog, "MAVLink.SigningKeys") + +Q_APPLICATION_STATIC(MAVLinkSigningKeys, _mavlinkSigningKeysInstance); + +// ── MAVLinkSigningKey ────────────────────────────────────────────────────── + +MAVLinkSigningKey::MAVLinkSigningKey(const QString& name, const QByteArray& keyBytes, QObject* parent) + : QObject(parent) + , _name(name) + , _keyBytes(keyBytes) +{ +} + +// ── MAVLinkSigningKeys ───────────────────────────────────────────────────── + +MAVLinkSigningKeys* MAVLinkSigningKeys::instance() +{ + return _mavlinkSigningKeysInstance(); +} + +MAVLinkSigningKeys::MAVLinkSigningKeys(QObject* parent) + : QObject(parent) + , _keys(new QmlObjectListModel(this)) +{ + _load(); + + auto* mvm = MultiVehicleManager::instance(); + connect(mvm, &MultiVehicleManager::vehicleAdded, this, &MAVLinkSigningKeys::_connectVehicle); + connect(mvm, &MultiVehicleManager::vehicleRemoved, this, &MAVLinkSigningKeys::_disconnectVehicle); + + // Wire any vehicles that already exist (in case singleton was created late) + for (int i = 0; i < mvm->vehicles()->count(); ++i) { + _connectVehicle(mvm->vehicles()->value(i)); + } +} + +MAVLinkSigningKeys::~MAVLinkSigningKeys() +{ +} + +bool MAVLinkSigningKeys::isKeyInUse(const QString& name) const +{ + auto* mvm = MultiVehicleManager::instance(); + for (int i = 0; i < mvm->vehicles()->count(); ++i) { + const auto* vehicle = mvm->vehicles()->value(i); + if (vehicle && vehicle->mavlinkSigningKeyName() == name) { + return true; + } + } + return false; +} + +void MAVLinkSigningKeys::_connectVehicle(Vehicle* vehicle) +{ + connect(vehicle, &Vehicle::mavlinkSigningChanged, this, &MAVLinkSigningKeys::keyUsageChanged); + emit keyUsageChanged(); +} + +void MAVLinkSigningKeys::_disconnectVehicle(Vehicle* vehicle) +{ + disconnect(vehicle, &Vehicle::mavlinkSigningChanged, this, &MAVLinkSigningKeys::keyUsageChanged); + emit keyUsageChanged(); +} + +QByteArray MAVLinkSigningKeys::keyBytesAt(int index) const +{ + if (index >= 0 && index < _keys->count()) { + return _keys->value(index)->keyBytes(); + } + return QByteArray(); +} + +QString MAVLinkSigningKeys::keyNameAt(int index) const +{ + if (index >= 0 && index < _keys->count()) { + return _keys->value(index)->name(); + } + return QString(); +} + +QByteArray MAVLinkSigningKeys::keyBytesByName(const QString& name) const +{ + for (int i = 0; i < _keys->count(); ++i) { + const auto* key = _keys->value(i); + if (key->name() == name) { + return key->keyBytes(); + } + } + return QByteArray(); +} + +void MAVLinkSigningKeys::addKey(const QString& name, const QString& passphrase) +{ + if (name.isEmpty() || passphrase.isEmpty()) { + qCWarning(MAVLinkSigningKeysLog) << "Name and passphrase must not be empty"; + return; + } + + // Check for duplicate name + for (int i = 0; i < _keys->count(); ++i) { + if (_keys->value(i)->name() == name) { + qCWarning(MAVLinkSigningKeysLog) << "Key with name already exists:" << name; + return; + } + } + + const QByteArray hash = QCryptographicHash::hash(passphrase.toUtf8(), QCryptographicHash::Sha256); + + auto* key = new MAVLinkSigningKey(name, hash, _keys); + _keys->append(key); + + _save(); + emit keysChanged(); +} + +void MAVLinkSigningKeys::removeKey(int index) +{ + if (index < 0 || index >= _keys->count()) { + return; + } + + _keys->removeAt(index)->deleteLater(); + _save(); + emit keysChanged(); +} + +void MAVLinkSigningKeys::_save() +{ + QSettings settings; + settings.beginGroup(kSettingsGroup); + + settings.beginWriteArray(kKeysArrayKey); + for (int i = 0; i < _keys->count(); ++i) { + settings.setArrayIndex(i); + const auto* key = _keys->value(i); + settings.setValue(kNameKey, key->name()); + settings.setValue(kKeyBytesKey, key->keyBytes().toHex()); + } + settings.endArray(); + + settings.endGroup(); +} + +void MAVLinkSigningKeys::_load() +{ + QSettings settings; + settings.beginGroup(kSettingsGroup); + + const int count = settings.beginReadArray(kKeysArrayKey); + for (int i = 0; i < count; ++i) { + settings.setArrayIndex(i); + const QString name = settings.value(kNameKey).toString(); + const QByteArray keyBytes = QByteArray::fromHex(settings.value(kKeyBytesKey).toByteArray()); + if (!name.isEmpty() && keyBytes.size() == 32) { + _keys->append(new MAVLinkSigningKey(name, keyBytes, _keys)); + } + } + settings.endArray(); + + settings.endGroup(); +} diff --git a/src/MAVLink/MAVLinkSigningKeys.h b/src/MAVLink/MAVLinkSigningKeys.h new file mode 100644 index 000000000000..d76ed11bb658 --- /dev/null +++ b/src/MAVLink/MAVLinkSigningKeys.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include + +class QmlObjectListModel; +class Vehicle; + +/// A single named signing key entry +class MAVLinkSigningKey : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + + Q_PROPERTY(QString name READ name CONSTANT) + +public: + explicit MAVLinkSigningKey(const QString& name, const QByteArray& keyBytes, QObject* parent = nullptr); + + QString name() const { return _name; } + QByteArray keyBytes() const { return _keyBytes; } + +private: + QString _name; + QByteArray _keyBytes; // 32-byte SHA-256 hash +}; + +/// Manages the collection of named MAVLink signing keys. +/// There is no "active key" concept. Keys are stored as a bag. The correct key +/// for each vehicle is auto-detected from incoming signed packets. +class MAVLinkSigningKeys : public QObject +{ + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(QmlObjectListModel* keys READ keys CONSTANT) + +public: + static MAVLinkSigningKeys* instance(); + + explicit MAVLinkSigningKeys(QObject* parent = nullptr); + ~MAVLinkSigningKeys() override; + + QmlObjectListModel* keys() const { return _keys; } + + /// Add a new named key. The passphrase is SHA-256 hashed; only the hash is stored. + Q_INVOKABLE void addKey(const QString& name, const QString& passphrase); + + /// Remove a key by index + Q_INVOKABLE void removeKey(int index); + + /// Returns true if any connected vehicle is using the key with the given name + Q_INVOKABLE bool isKeyInUse(const QString& name) const; + + /// Returns the key bytes for a key at the given index, or empty if invalid + QByteArray keyBytesAt(int index) const; + + /// Returns the key bytes for the key with the given name, or empty if not found + QByteArray keyBytesByName(const QString& name) const; + + /// Returns the key name at the given index, or empty if invalid + QString keyNameAt(int index) const; + +signals: + void keysChanged(); + void keyUsageChanged(); + +private: + void _save(); + void _load(); + void _connectVehicle(Vehicle* vehicle); + void _disconnectVehicle(Vehicle* vehicle); + + QmlObjectListModel* _keys = nullptr; + + static constexpr const char* kSettingsGroup = "MAVLinkSigningKeys"; + static constexpr const char* kKeysArrayKey = "keys"; + static constexpr const char* kNameKey = "name"; + static constexpr const char* kKeyBytesKey = "keyBytes"; +}; + diff --git a/src/QmlControls/CMakeLists.txt b/src/QmlControls/CMakeLists.txt index 35fc27e749b7..019dc7a481c5 100644 --- a/src/QmlControls/CMakeLists.txt +++ b/src/QmlControls/CMakeLists.txt @@ -168,6 +168,7 @@ qt_add_qml_module(QGroundControlControlsModule SettingsButton.qml SettingsGroupLayout.qml SetupPage.qml + SigningKeyManager.qml SliderSwitch.qml SubMenuButton.qml TimedProgressTracker.qml diff --git a/src/QmlControls/QGroundControlQmlGlobal.cc b/src/QmlControls/QGroundControlQmlGlobal.cc index 5f9b7e5ee454..6ff5e2896e34 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.cc +++ b/src/QmlControls/QGroundControlQmlGlobal.cc @@ -12,6 +12,7 @@ #include "QGCMapEngineManager.h" #include "ADSBVehicleManager.h" #include "NTRIPManager.h" +#include "MAVLinkSigningKeys.h" #include "MissionCommandTree.h" #include "VideoManager.h" #include "MultiVehicleManager.h" @@ -39,6 +40,7 @@ QGroundControlQmlGlobal::QGroundControlQmlGlobal(QObject *parent) , _ntripManager(NTRIPManager::instance()) , _qgcPositionManager(QGCPositionManager::instance()) , _missionCommandTree(MissionCommandTree::instance()) + , _mavlinkSigningKeys(MAVLinkSigningKeys::instance()) , _videoManager(VideoManager::instance()) , _linkManager(LinkManager::instance()) , _multiVehicleManager(MultiVehicleManager::instance()) diff --git a/src/QmlControls/QGroundControlQmlGlobal.h b/src/QmlControls/QGroundControlQmlGlobal.h index 2c05c8bfbe98..efaa0b70904b 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.h +++ b/src/QmlControls/QGroundControlQmlGlobal.h @@ -15,6 +15,7 @@ Q_DECLARE_LOGGING_CATEGORY(GuidedActionsControllerLog) class ADSBVehicleManager; class FactGroup; class LinkManager; +class MAVLinkSigningKeys; class MissionCommandTree; class MultiVehicleManager; class QGCCorePlugin; @@ -30,6 +31,7 @@ Q_MOC_INCLUDE("ADSBVehicleManager.h") Q_MOC_INCLUDE("NTRIPManager.h") Q_MOC_INCLUDE("FactGroup.h") Q_MOC_INCLUDE("LinkManager.h") +Q_MOC_INCLUDE("MAVLinkSigningKeys.h") Q_MOC_INCLUDE("MissionCommandTree.h") Q_MOC_INCLUDE("MultiVehicleManager.h") Q_MOC_INCLUDE("QGCCorePlugin.h") @@ -72,6 +74,7 @@ class QGroundControlQmlGlobal : public QObject Q_PROPERTY(NTRIPManager* ntripManager READ ntripManager CONSTANT) Q_PROPERTY(QGCCorePlugin* corePlugin READ corePlugin CONSTANT) Q_PROPERTY(MissionCommandTree* missionCommandTree READ missionCommandTree CONSTANT) + Q_PROPERTY(MAVLinkSigningKeys* mavlinkSigningKeys READ mavlinkSigningKeys CONSTANT) #ifndef QGC_NO_SERIAL_LINK Q_PROPERTY(FactGroup* gpsRtk READ gpsRtkFactGroup CONSTANT) #endif @@ -168,6 +171,7 @@ class QGroundControlQmlGlobal : public QObject QGCMapEngineManager* mapEngineManager () { return _mapEngineManager; } QGCPositionManager* qgcPositionManger () { return _qgcPositionManager; } MissionCommandTree* missionCommandTree () { return _missionCommandTree; } + MAVLinkSigningKeys* mavlinkSigningKeys () { return _mavlinkSigningKeys; } VideoManager* videoManager () { return _videoManager; } QGCCorePlugin* corePlugin () { return _corePlugin; } SettingsManager* settingsManager () { return _settingsManager; } @@ -229,6 +233,7 @@ class QGroundControlQmlGlobal : public QObject NTRIPManager* _ntripManager = nullptr; QGCPositionManager* _qgcPositionManager = nullptr; MissionCommandTree* _missionCommandTree = nullptr; + MAVLinkSigningKeys* _mavlinkSigningKeys = nullptr; VideoManager* _videoManager = nullptr; LinkManager* _linkManager = nullptr; MultiVehicleManager* _multiVehicleManager = nullptr; diff --git a/src/QmlControls/SigningKeyManager.qml b/src/QmlControls/SigningKeyManager.qml new file mode 100644 index 000000000000..c9d0c0b8b82a --- /dev/null +++ b/src/QmlControls/SigningKeyManager.qml @@ -0,0 +1,128 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls + +SettingsGroupLayout { + id: _signingKeyManager + + Layout.fillWidth: true + heading: qsTr("MAVLink 2 Signing") + headingDescription: qsTr("Signing keys should only be sent to the vehicle over secure links.") + + property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle + + // Reactive key-usage tracking (re-evaluated on any vehicle's signing change) + property int _keyUsageRevision: 0 + Connections { + target: QGroundControl.mavlinkSigningKeys + function onKeyUsageChanged() { _keyUsageRevision++ } + } + + QGCPopupDialogFactory { + id: addKeyDialogFactory + dialogComponent: addKeyDialogComponent + } + + Component { + id: addKeyDialogComponent + + QGCPopupDialog { + title: qsTr("Add Signing Key") + buttons: Dialog.Ok | Dialog.Cancel + acceptButtonEnabled: keyNameField.text !== "" && keyPassphraseField.text !== "" + + onAccepted: { + QGroundControl.mavlinkSigningKeys.addKey(keyNameField.text, keyPassphraseField.text) + } + + ColumnLayout { + spacing: ScreenTools.defaultFontPixelHeight / 2 + + QGCLabel { text: qsTr("Key Name") } + QGCTextField { + id: keyNameField + Layout.fillWidth: true + Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 30 + placeholderText: qsTr("Enter a friendly name") + } + + QGCLabel { text: qsTr("Passphrase") } + QGCTextField { + id: keyPassphraseField + Layout.fillWidth: true + Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 30 + placeholderText: qsTr("Enter passphrase") + echoMode: TextInput.Password + } + } + } + } + + LabelledLabel { + Layout.fillWidth: true + label: qsTr("Signing Status") + labelText: _activeVehicle ? (_activeVehicle.mavlinkSigning ? qsTr("Active") : qsTr("Inactive")) : qsTr("Not Connected") + } + + LabelledLabel { + Layout.fillWidth: true + label: qsTr("Active Key") + labelText: _activeVehicle && _activeVehicle.mavlinkSigningKeyName !== "" ? _activeVehicle.mavlinkSigningKeyName : qsTr("None") + visible: _activeVehicle + } + + Repeater { + model: QGroundControl.mavlinkSigningKeys.keys + + RowLayout { + Layout.fillWidth: true + spacing: ScreenTools.defaultFontPixelWidth + + property bool _keyIsActive: { void(_keyUsageRevision); return QGroundControl.mavlinkSigningKeys.isKeyInUse(object.name) } + property bool _keyIsActiveVehicle: _activeVehicle && _activeVehicle.mavlinkSigningKeyName === object.name + property bool _anyKeyActive: _activeVehicle && _activeVehicle.mavlinkSigningKeyName !== "" + + QGCLabel { + text: object.name + Layout.fillWidth: true + } + + QGCButton { + text: qsTr("Enable") + visible: !parent._anyKeyActive + enabled: _activeVehicle + onClicked: _activeVehicle.sendSetupSigning(index) + } + + QGCButton { + text: qsTr("Disable") + visible: parent._keyIsActiveVehicle + onClicked: _activeVehicle.sendDisableSigning() + } + + QGCButton { + text: qsTr("Delete") + visible: !parent._keyIsActive + onClicked: QGroundControl.showMessageDialog( + _signingKeyManager, + qsTr("Delete Signing Key"), + qsTr("Are you sure you want to delete '%1'? If a vehicle still has this key configured, you will no longer be able to communicate with it over a signed connection.").arg(object.name), + Dialog.Ok | Dialog.Cancel, + function () { QGroundControl.mavlinkSigningKeys.removeKey(index) }) + } + } + } + + QGCLabel { + text: qsTr("No keys configured") + visible: QGroundControl.mavlinkSigningKeys.keys.count === 0 + } + + QGCButton { + text: qsTr("Add Key") + onClicked: addKeyDialogFactory.open() + } +} diff --git a/src/Settings/Mavlink.SettingsGroup.json b/src/Settings/Mavlink.SettingsGroup.json index 632336f1aa29..aa206bce4709 100644 --- a/src/Settings/Mavlink.SettingsGroup.json +++ b/src/Settings/Mavlink.SettingsGroup.json @@ -53,12 +53,6 @@ "type": "string", "default": "support.ardupilot.org:xxxx" }, -{ - "name": "mavlink2SigningKey", - "shortDesc": "MAVLink 2.0 signing key", - "type": "string", - "default": "" -}, { "name": "sendGCSHeartbeat", "shortDesc": "Send GCS Heartbeat", diff --git a/src/Settings/MavlinkSettings.cc b/src/Settings/MavlinkSettings.cc index 215755587b76..da378751f284 100644 --- a/src/Settings/MavlinkSettings.cc +++ b/src/Settings/MavlinkSettings.cc @@ -1,5 +1,4 @@ #include "MavlinkSettings.h" -#include "LinkManager.h" DECLARE_SETTINGGROUP(Mavlink, "") { @@ -37,17 +36,3 @@ DECLARE_SETTINGSFACT(MavlinkSettings, forwardMavlinkHostName) DECLARE_SETTINGSFACT(MavlinkSettings, forwardMavlinkAPMSupportHostName) DECLARE_SETTINGSFACT(MavlinkSettings, sendGCSHeartbeat) DECLARE_SETTINGSFACT(MavlinkSettings, gcsMavlinkSystemID) - -DECLARE_SETTINGSFACT_NO_FUNC(MavlinkSettings, mavlink2SigningKey) -{ - if (!_mavlink2SigningKeyFact) { - _mavlink2SigningKeyFact = _createSettingsFact(mavlink2SigningKeyName); - connect(_mavlink2SigningKeyFact, &Fact::rawValueChanged, this, &MavlinkSettings::_mavlink2SigningKeyChanged); - } - return _mavlink2SigningKeyFact; -} - -void MavlinkSettings::_mavlink2SigningKeyChanged(void) -{ - LinkManager::instance()->resetMavlinkSigning(); -} diff --git a/src/Settings/MavlinkSettings.h b/src/Settings/MavlinkSettings.h index 81fe1f70f0bd..bc888d5f3feb 100644 --- a/src/Settings/MavlinkSettings.h +++ b/src/Settings/MavlinkSettings.h @@ -21,13 +21,9 @@ class MavlinkSettings : public SettingsGroup DEFINE_SETTINGFACT(forwardMavlink) DEFINE_SETTINGFACT(forwardMavlinkHostName) DEFINE_SETTINGFACT(forwardMavlinkAPMSupportHostName) - DEFINE_SETTINGFACT(mavlink2SigningKey) DEFINE_SETTINGFACT(sendGCSHeartbeat) DEFINE_SETTINGFACT(gcsMavlinkSystemID) // Although this is a global setting it only affects ArduPilot vehicle since PX4 automatically starts the stream from the vehicle side DEFINE_SETTINGFACT(apmStartMavlinkStreams) - -private slots: - void _mavlink2SigningKeyChanged(); }; diff --git a/src/UI/AppSettings/TelemetrySettings.qml b/src/UI/AppSettings/TelemetrySettings.qml index d2be9037e1ed..53f7ab728dcf 100644 --- a/src/UI/AppSettings/TelemetrySettings.qml +++ b/src/UI/AppSettings/TelemetrySettings.qml @@ -35,47 +35,7 @@ SettingsPage { } } - SettingsGroupLayout { - id: mavlink2SigningGroup - Layout.fillWidth: true - heading: qsTr("MAVLink 2 Signing") - headingDescription: qsTr("Signing keys should only be sent to the vehicle over secure links.") - visible: _mavlink2SigningKey.visible - - property Fact _mavlink2SigningKey: _mavlinkSettings.mavlink2SigningKey - - Connections { - target: mavlink2SigningGroup._mavlink2SigningKey - function onRawValueChanged(value) { sendToVehiclePrompt.visible = true } - } - - RowLayout { - spacing: ScreenTools.defaultFontPixelWidth - - LabelledFactTextField { - Layout.fillWidth: true - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 32 - label: qsTr("Key") - fact: mavlink2SigningGroup._mavlink2SigningKey - } - - QGCButton { - text: qsTr("Send to Vehicle") - enabled: _activeVehicle - - onClicked: { - sendToVehiclePrompt.visible = false - _activeVehicle.sendSetupSigning() - } - } - } - - QGCLabel { - id: sendToVehiclePrompt - Layout.fillWidth: true - text: qsTr("Signing key has changed. Don't forget to send to Vehicle(s) if needed.") - visible: false - } + SigningKeyManager { } SettingsGroupLayout { diff --git a/src/UI/toolbar/CMakeLists.txt b/src/UI/toolbar/CMakeLists.txt index 7fe6ab1d913e..72e71133b05d 100644 --- a/src/UI/toolbar/CMakeLists.txt +++ b/src/UI/toolbar/CMakeLists.txt @@ -31,6 +31,7 @@ qt_add_qml_module(ToolbarModule RTKGPSIndicator.qml SelectViewDropdown.qml SignalStrength.qml + SigningIndicator.qml TelemetryRSSIIndicator.qml VehicleGPSIndicator.qml VehicleMessageList.qml diff --git a/src/UI/toolbar/SigningIndicator.qml b/src/UI/toolbar/SigningIndicator.qml new file mode 100644 index 000000000000..d30b8430edcb --- /dev/null +++ b/src/UI/toolbar/SigningIndicator.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls + +Item { + id: control + width: signingIcon.width * 1.1 + anchors.top: parent.top + anchors.bottom: parent.bottom + + property bool showIndicator: QGroundControl.mavlinkSigningKeys.keys.count > 0 + + property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle + property bool _signed: _activeVehicle ? _activeVehicle.mavlinkSigning : false + + QGCPalette { id: qgcPal } + + QGCColoredImage { + id: signingIcon + anchors.top: parent.top + anchors.bottom: parent.bottom + width: height + sourceSize.height: height + source: _signed ? "/InstrumentValueIcons/lock-closed.svg" : "/InstrumentValueIcons/lock-open.svg" + fillMode: Image.PreserveAspectFit + color: _signed ? qgcPal.colorGreen : qgcPal.text + } + + MouseArea { + anchors.fill: parent + onClicked: mainWindow.showIndicatorDrawer(signingInfoPage, control) + } + + Component { + id: signingInfoPage + + ToolIndicatorPage { + showExpand: true + contentComponent: signingContentComponent + expandedComponent: signingExpandedComponent + } + } + + Component { + id: signingContentComponent + + SettingsGroupLayout { + heading: qsTr("MAVLink Signing") + + LabelledLabel { + label: qsTr("Status:") + labelText: { + if (!_activeVehicle) + return qsTr("No Vehicle") + return _signed ? qsTr("Active") : qsTr("Inactive") + } + } + + LabelledLabel { + label: qsTr("Active Key:") + labelText: _activeVehicle && _activeVehicle.mavlinkSigningKeyName !== "" ? _activeVehicle.mavlinkSigningKeyName : qsTr("None") + visible: _activeVehicle + } + + LabelledLabel { + label: qsTr("Saved Keys:") + labelText: QGroundControl.mavlinkSigningKeys.keys.count + } + } + } + + Component { + id: signingExpandedComponent + + SigningKeyManager { + } + } +} diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index 0804b06a07dd..42f446b81a00 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -65,6 +65,7 @@ #include "DeviceInfo.h" #include "StatusTextHandler.h" #include "MAVLinkSigning.h" +#include "MAVLinkSigningKeys.h" #include "GimbalController.h" #include "MavlinkSettings.h" #include "APM.h" @@ -508,6 +509,15 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes } } + // Try to auto-detect signing key from incoming signed packets + if (MAVLinkSigning::isMessageSigned(message) && !MAVLinkSigning::isSigningEnabled(static_cast(link->mavlinkChannel()))) { + const QString detectedKeyName = MAVLinkSigning::tryDetectKey(static_cast(link->mavlinkChannel()), message); + if (!detectedKeyName.isEmpty() && link == vehicleLinkManager()->primaryLink().lock().get()) { + _mavlinkSigningKeyName = detectedKeyName; + emit mavlinkSigningChanged(); + } + } + // We give the link manager first whack since it it reponsible for adding new links _vehicleLinkManager->mavlinkMessageReceived(link, message); @@ -3701,6 +3711,20 @@ void Vehicle::_mavlinkMessageStatus(int uasId, uint64_t totalSent, uint64_t tota _mavlinkLossCount = totalLoss; _mavlinkLossPercent = lossPercent; emit mavlinkStatusChanged(); + + // Update signing status from the primary link's channel + bool signing = false; + SharedLinkInterfacePtr sharedLink = vehicleLinkManager()->primaryLink().lock(); + if (sharedLink) { + signing = MAVLinkSigning::isSigningEnabled(static_cast(sharedLink->mavlinkChannel())); + } + if (signing != _mavlinkSigning) { + _mavlinkSigning = signing; + if (!signing) { + _mavlinkSigningKeyName.clear(); + } + emit mavlinkSigningChanged(); + } } } @@ -4548,7 +4572,7 @@ void Vehicle::_errorMessageReceived(QString message) /* Signing */ /*===========================================================================*/ -void Vehicle::sendSetupSigning() +void Vehicle::sendSetupSigning(int keyIndex) { SharedLinkInterfacePtr sharedLink = vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { @@ -4556,6 +4580,12 @@ void Vehicle::sendSetupSigning() return; } + const QByteArray keyBytes = MAVLinkSigningKeys::instance()->keyBytesAt(keyIndex); + if (keyBytes.isEmpty()) { + qCCritical(VehicleLog) << "Invalid key index:" << keyIndex; + return; + } + const mavlink_channel_t channel = static_cast(sharedLink->mavlinkChannel()); mavlink_setup_signing_t setup_signing; @@ -4564,7 +4594,17 @@ void Vehicle::sendSetupSigning() target_system.sysid = id(); target_system.compid = defaultComponentId(); - MAVLinkSigning::createSetupSigning(channel, target_system, setup_signing); + MAVLinkSigning::createSetupSigning(channel, target_system, keyBytes, setup_signing); + + // Also configure signing on our channel with this key so outgoing packets are signed + if (!MAVLinkSigning::initSigning(channel, keyBytes, MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)) { + qCCritical(VehicleLog) << "Internal error: failed to initialize signing on channel" << channel; + return; + } + + _mavlinkSigning = true; + _mavlinkSigningKeyName = MAVLinkSigningKeys::instance()->keyNameAt(keyIndex); + emit mavlinkSigningChanged(); mavlink_message_t msg; (void) mavlink_msg_setup_signing_encode_chan(MAVLinkProtocol::instance()->getSystemId(), MAVLinkProtocol::getComponentId(), channel, &msg, &setup_signing); @@ -4575,6 +4615,39 @@ void Vehicle::sendSetupSigning() } } +void Vehicle::sendDisableSigning() +{ + SharedLinkInterfacePtr sharedLink = vehicleLinkManager()->primaryLink().lock(); + if (!sharedLink) { + qCDebug(VehicleLog) << "Primary Link Gone!"; + return; + } + + const mavlink_channel_t channel = static_cast(sharedLink->mavlinkChannel()); + + mavlink_setup_signing_t setup_signing; + + mavlink_system_t target_system; + target_system.sysid = id(); + target_system.compid = defaultComponentId(); + + MAVLinkSigning::createDisableSigning(target_system, setup_signing); + + mavlink_message_t msg; + (void) mavlink_msg_setup_signing_encode_chan(MAVLinkProtocol::instance()->getSystemId(), MAVLinkProtocol::getComponentId(), channel, &msg, &setup_signing); + + for (uint8_t i = 0; i < 2; ++i) { + sendMessageOnLinkThreadSafe(sharedLink.get(), msg); + } + + // Disable signing on the local channel so we stop signing outgoing packets + MAVLinkSigning::initSigning(channel, QByteArrayView(), nullptr); + + _mavlinkSigning = false; + _mavlinkSigningKeyName.clear(); + emit mavlinkSigningChanged(); +} + /*---------------------------------------------------------------------------*/ /*===========================================================================*/ /* Image Protocol Manager */ diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index 952b87267901..f7e50ec544b1 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -271,6 +271,7 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(QString vehicleUIDStr READ vehicleUIDStr NOTIFY vehicleUIDChanged) Q_PROPERTY(bool mavlinkSigning READ mavlinkSigning NOTIFY mavlinkSigningChanged) + Q_PROPERTY(QString mavlinkSigningKeyName READ mavlinkSigningKeyName NOTIFY mavlinkSigningChanged) /// Resets link status counters Q_INVOKABLE void resetCounters (); @@ -396,7 +397,9 @@ class Vehicle : public VehicleFactGroup /// Set home from flight map coordinate Q_INVOKABLE void doSetHome(const QGeoCoordinate& coord); - Q_INVOKABLE void sendSetupSigning(); + /// Send SETUP_SIGNING with the key at the given index in MAVLinkSigningKeys + Q_INVOKABLE void sendSetupSigning(int keyIndex); + Q_INVOKABLE void sendDisableSigning(); Q_INVOKABLE QVariant expandedToolbarIndicatorSource(const QString& indicatorName); @@ -544,6 +547,7 @@ class Vehicle : public VehicleFactGroup bool hilMode () const { return _base_mode & MAV_MODE_FLAG_HIL_ENABLED; } Actuators* actuators () const { return _actuators; } bool mavlinkSigning () const { return _mavlinkSigning; } + QString mavlinkSigningKeyName () const { return _mavlinkSigningKeyName; } void startCalibration (QGCMAVLink::CalibrationType calType); void stopCalibration (bool showError); @@ -1014,6 +1018,7 @@ void _activeVehicleChanged (Vehicle* newActiveVehicle); bool _readyToFly = false; bool _allSensorsHealthy = true; bool _mavlinkSigning = false; + QString _mavlinkSigningKeyName; SysStatusSensorInfo _sysStatusSensorInfo; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 952b9fb4a8d3..cc492e2aed87 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -149,6 +149,7 @@ add_qgc_test(SDLTest LABELS Unit Joystick RESOURCE_LOCK Joystick) # ---------------------------------------------------------------------------- add_subdirectory(MAVLink) add_qgc_test(SigningTest LABELS Unit MAVLink) +add_qgc_test(MockLinkSigningTest LABELS Integration Vehicle MAVLink RESOURCE_LOCK MockLink) add_qgc_test(StatusTextHandlerTest LABELS Unit MAVLink) add_qgc_test(SysStatusSensorInfoTest LABELS Unit MAVLink) diff --git a/test/MAVLink/CMakeLists.txt b/test/MAVLink/CMakeLists.txt index 6484e8214891..1f7c81b0f323 100644 --- a/test/MAVLink/CMakeLists.txt +++ b/test/MAVLink/CMakeLists.txt @@ -5,6 +5,8 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE + MockLinkSigningTest.cc + MockLinkSigningTest.h SigningTest.cc SigningTest.h StatusTextHandlerTest.cc diff --git a/test/MAVLink/MockLinkSigningTest.cc b/test/MAVLink/MockLinkSigningTest.cc new file mode 100644 index 000000000000..05d21abe0b65 --- /dev/null +++ b/test/MAVLink/MockLinkSigningTest.cc @@ -0,0 +1,125 @@ +#include "MockLinkSigningTest.h" + +#include "MAVLinkSigningKeys.h" +#include "MAVLinkSigning.h" +#include "Vehicle.h" + +#include + +MockLinkSigningTest::MockLinkSigningTest(QObject* parent) + : VehicleTest(parent) +{ +} + +void MockLinkSigningTest::init() +{ + VehicleTest::init(); + + // Ensure a clean key store before each test + auto* signingKeys = MAVLinkSigningKeys::instance(); + while (signingKeys->keys()->count() > 0) { + signingKeys->removeKey(0); + } +} + +void MockLinkSigningTest::cleanup() +{ + // Always clean up keys even if the test failed + auto* signingKeys = MAVLinkSigningKeys::instance(); + while (signingKeys->keys()->count() > 0) { + signingKeys->removeKey(0); + } + + VehicleTest::cleanup(); +} + +void MockLinkSigningTest::_testSendSetupSigning() +{ + QVERIFY(vehicle()); + QVERIFY(mockLink()); + + // Add a key + auto* signingKeys = MAVLinkSigningKeys::instance(); + signingKeys->addKey("TestKey", "TestPassphrase"); + QVERIFY(!signingKeys->keyBytesAt(0).isEmpty()); + + mockLink()->clearReceivedMavlinkMessageCounts(); + QVERIFY(!mockLink()->signingEnabled()); + + QSignalSpy signingChangedSpy(vehicle(), &Vehicle::mavlinkSigningChanged); + + // Send signing setup to vehicle with key index 0 + vehicle()->sendSetupSigning(0); + + // Verify MockLink received the SETUP_SIGNING message (sent twice for reliability) + QVERIFY_TRUE_WAIT(mockLink()->receivedMavlinkMessageCount(MAVLINK_MSG_ID_SETUP_SIGNING) >= 2, TestTimeout::mediumMs()); + QVERIFY(mockLink()->signingEnabled()); + + // Verify the signal was emitted and the vehicle tracks the active key name + QVERIFY(signingChangedSpy.count() > 0); + QCOMPARE(vehicle()->mavlinkSigningKeyName(), QStringLiteral("TestKey")); +} + +void MockLinkSigningTest::_testSendDisableSigning() +{ + QVERIFY(vehicle()); + QVERIFY(mockLink()); + + // Add a key and send to vehicle first + auto* signingKeys = MAVLinkSigningKeys::instance(); + signingKeys->addKey("TestKey2", "TestPassphrase2"); + + QSignalSpy signingChangedSpy(vehicle(), &Vehicle::mavlinkSigningChanged); + + vehicle()->sendSetupSigning(0); + QVERIFY_TRUE_WAIT(mockLink()->signingEnabled(), TestTimeout::mediumMs()); + QVERIFY(signingChangedSpy.count() > 0); + + // Now disable signing + signingChangedSpy.clear(); + mockLink()->clearReceivedMavlinkMessageCounts(); + vehicle()->sendDisableSigning(); + + // Verify MockLink received the disable message and signing is now off + QVERIFY_TRUE_WAIT(mockLink()->receivedMavlinkMessageCount(MAVLINK_MSG_ID_SETUP_SIGNING) >= 2, TestTimeout::mediumMs()); + QVERIFY(!mockLink()->signingEnabled()); + + // Verify the signal was emitted and the vehicle cleared the active key name + QVERIFY(signingChangedSpy.count() > 0); + QVERIFY(vehicle()->mavlinkSigningKeyName().isEmpty()); +} + +void MockLinkSigningTest::_testSigningKeysAddRemove() +{ + auto* signingKeys = MAVLinkSigningKeys::instance(); + + // Start with no keys + QCOMPARE(signingKeys->keys()->count(), 0); + + // Add first key + QSignalSpy spy(signingKeys, &MAVLinkSigningKeys::keysChanged); + signingKeys->addKey("Key1", "pass1"); + QCOMPARE(signingKeys->keys()->count(), 1); + QCOMPARE(signingKeys->keyNameAt(0), "Key1"); + QVERIFY(!signingKeys->keyBytesAt(0).isEmpty()); + QCOMPARE(signingKeys->keyBytesAt(0).size(), 32); + QVERIFY(spy.count() > 0); + + // Add second key + spy.clear(); + signingKeys->addKey("Key2", "pass2"); + QCOMPARE(signingKeys->keys()->count(), 2); + QCOMPARE(signingKeys->keyNameAt(1), "Key2"); + QVERIFY(spy.count() > 0); + + // Remove first key + signingKeys->removeKey(0); + QCOMPARE(signingKeys->keys()->count(), 1); + QCOMPARE(signingKeys->keyNameAt(0), "Key2"); + + // Remove remaining key + signingKeys->removeKey(0); + QCOMPARE(signingKeys->keys()->count(), 0); +} + +UT_REGISTER_TEST(MockLinkSigningTest, TestLabel::Integration, TestLabel::Vehicle) diff --git a/test/MAVLink/MockLinkSigningTest.h b/test/MAVLink/MockLinkSigningTest.h new file mode 100644 index 000000000000..e4f077bd3dca --- /dev/null +++ b/test/MAVLink/MockLinkSigningTest.h @@ -0,0 +1,18 @@ +#pragma once + +#include "BaseClasses/VehicleTest.h" + +class MockLinkSigningTest : public VehicleTest +{ + Q_OBJECT + +public: + explicit MockLinkSigningTest(QObject* parent = nullptr); + +private slots: + void init() override; + void cleanup() override; + void _testSendSetupSigning(); + void _testSendDisableSigning(); + void _testSigningKeysAddRemove(); +}; diff --git a/test/MAVLink/SigningTest.cc b/test/MAVLink/SigningTest.cc index 07a2be7091ca..9d21049ae3a3 100644 --- a/test/MAVLink/SigningTest.cc +++ b/test/MAVLink/SigningTest.cc @@ -1,30 +1,34 @@ #include "SigningTest.h" #include "MAVLinkSigning.h" +#include "MAVLinkSigningKeys.h" +#include "QmlObjectListModel.h" void SigningTest::_testInitSigning() { - QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, "secret_key", - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); + // Create a 32-byte raw key + QByteArray rawKey(32, '\0'); + rawKey[0] = 'A'; + rawKey[1] = 'B'; + + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, rawKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); const mavlink_status_t* status = mavlink_get_channel_status(MAVLINK_COMM_0); const mavlink_signing_t* signing = status->signing; - QVERIFY(memcmp(signing->secret_key, QCryptographicHash::hash("secret_key", QCryptographicHash::Sha256).constData(), - sizeof(signing->secret_key)) == 0); - QByteArray keyData(32, '\0'); - keyData[0] = '1'; - QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, keyData, - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); - QVERIFY(memcmp(signing->secret_key, - QCryptographicHash::hash(keyData, QCryptographicHash::Sha256).constData(), - sizeof(signing->secret_key)) == 0); + QVERIFY(signing); + QVERIFY(memcmp(signing->secret_key, rawKey.constData(), sizeof(signing->secret_key)) == 0); + + // Disable signing with empty key QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, QByteArrayView(), - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); + QVERIFY(!mavlink_get_channel_status(MAVLINK_COMM_0)->signing); } void SigningTest::_testCheckSigningLinkId() { - QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, "secret_key", - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); + QByteArray rawKey(32, '\x01'); + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, rawKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); const mavlink_heartbeat_t heartbeat{}; mavlink_message_t message; (void)mavlink_msg_heartbeat_encode_chan(1, MAV_COMP_ID_USER1, MAVLINK_COMM_0, &message, &heartbeat); @@ -33,16 +37,101 @@ void SigningTest::_testCheckSigningLinkId() void SigningTest::_testCreateSetupSigning() { - QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, "secret_key", - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); + QByteArray rawKey(32, '\x02'); + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, rawKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); mavlink_system_t target_system{}; target_system.sysid = 1; target_system.compid = MAV_COMP_ID_AUTOPILOT1; mavlink_setup_signing_t setup_signing; - MAVLinkSigning::createSetupSigning(MAVLINK_COMM_0, target_system, setup_signing); + MAVLinkSigning::createSetupSigning(MAVLINK_COMM_0, target_system, rawKey, setup_signing); QVERIFY(setup_signing.initial_timestamp != 0); QCOMPARE(setup_signing.target_system, target_system.sysid); QCOMPARE(setup_signing.target_component, target_system.compid); + QVERIFY(memcmp(setup_signing.secret_key, rawKey.constData(), sizeof(setup_signing.secret_key)) == 0); +} + +void SigningTest::_testVerifySignature() +{ + // Set up signing on MAVLINK_COMM_1 with a known key so the C library signs outgoing messages + QByteArray rawKey(32, '\0'); + for (int i = 0; i < 32; ++i) { + rawKey[i] = static_cast(i); + } + + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_1, rawKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); + + // Encode a heartbeat on the signing channel — the C library will sign it + const mavlink_heartbeat_t heartbeat{}; + mavlink_message_t message; + (void) mavlink_msg_heartbeat_encode_chan(1, MAV_COMP_ID_AUTOPILOT1, MAVLINK_COMM_1, &message, &heartbeat); + + // The message should be signed + QVERIFY(MAVLinkSigning::isMessageSigned(message)); + + // Our verifySignature should agree with the C library's signature + QVERIFY(MAVLinkSigning::verifySignature(rawKey, message)); + + // A different key should NOT verify + QByteArray wrongKey(32, '\xFF'); + QVERIFY(!MAVLinkSigning::verifySignature(wrongKey, message)); + + // Too-short key should fail gracefully + QByteArray shortKey(16, '\x01'); + QVERIFY(!MAVLinkSigning::verifySignature(shortKey, message)); + + // Disable signing on the channel to clean up + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_1, QByteArrayView(), nullptr)); +} + +void SigningTest::_testTryDetectKey() +{ + auto* signingKeys = MAVLinkSigningKeys::instance(); + + // Clean slate — remove any leftover keys + while (signingKeys->keys()->count() > 0) { + signingKeys->removeKey(0); + } + + // Add three keys with different passphrases + signingKeys->addKey(QStringLiteral("KeyAlpha"), QStringLiteral("passphrase-alpha")); + signingKeys->addKey(QStringLiteral("KeyBravo"), QStringLiteral("passphrase-bravo")); + signingKeys->addKey(QStringLiteral("KeyCharlie"), QStringLiteral("passphrase-charlie")); + QCOMPARE(signingKeys->keys()->count(), 3); + + // Sign a message using the second key ("KeyBravo") + const QByteArray bravoKey = signingKeys->keyBytesAt(1); + QCOMPARE(bravoKey.size(), 32); + + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_2, bravoKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); + + const mavlink_heartbeat_t heartbeat{}; + mavlink_message_t message; + (void) mavlink_msg_heartbeat_encode_chan(1, MAV_COMP_ID_AUTOPILOT1, MAVLINK_COMM_2, &message, &heartbeat); + QVERIFY(MAVLinkSigning::isMessageSigned(message)); + + // Disable signing on COMM_2 so it was only used to produce the signed message + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_2, QByteArrayView(), nullptr)); + + // COMM_3 has no signing configured — tryDetectKey should find "KeyBravo" + QVERIFY(!MAVLinkSigning::isSigningEnabled(MAVLINK_COMM_3)); + const QString detected = MAVLinkSigning::tryDetectKey(MAVLINK_COMM_3, message); + QCOMPARE(detected, QStringLiteral("KeyBravo")); + + // Signing should now be configured on COMM_3 with the bravo key + QVERIFY(MAVLinkSigning::isSigningEnabled(MAVLINK_COMM_3)); + + // Calling tryDetectKey again on an already-configured channel should return empty + const QString again = MAVLinkSigning::tryDetectKey(MAVLINK_COMM_3, message); + QVERIFY(again.isEmpty()); + + // Clean up + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_3, QByteArrayView(), nullptr)); + while (signingKeys->keys()->count() > 0) { + signingKeys->removeKey(0); + } } UT_REGISTER_TEST(SigningTest, TestLabel::Unit) diff --git a/test/MAVLink/SigningTest.h b/test/MAVLink/SigningTest.h index 65bfcdfa04f5..e33e29d6c26d 100644 --- a/test/MAVLink/SigningTest.h +++ b/test/MAVLink/SigningTest.h @@ -10,4 +10,6 @@ private slots: void _testInitSigning(); void _testCheckSigningLinkId(); void _testCreateSetupSigning(); + void _testVerifySignature(); + void _testTryDetectKey(); }; From f586f8eecdf62af7ebc15492b2b524db9ce126d9 Mon Sep 17 00:00:00 2001 From: Ahmad Wael Date: Thu, 19 Mar 2026 21:44:33 +0200 Subject: [PATCH 118/119] APM: add Servo Outputs setup page with live output bars --- .../APM/APMAutoPilotPlugin.cc | 7 + src/AutoPilotPlugins/APM/APMAutoPilotPlugin.h | 2 + src/AutoPilotPlugins/APM/APMServoComponent.cc | 11 + src/AutoPilotPlugins/APM/APMServoComponent.h | 24 ++ .../APM/APMServoComponent.qml | 398 ++++++++++++++++++ src/AutoPilotPlugins/APM/CMakeLists.txt | 3 + src/QmlControls/CMakeLists.txt | 2 + .../ServoOutputMonitorController.cc | 41 ++ .../ServoOutputMonitorController.h | 35 ++ src/Vehicle/Vehicle.cc | 25 ++ src/Vehicle/Vehicle.h | 7 + 11 files changed, 555 insertions(+) create mode 100644 src/AutoPilotPlugins/APM/APMServoComponent.cc create mode 100644 src/AutoPilotPlugins/APM/APMServoComponent.h create mode 100644 src/AutoPilotPlugins/APM/APMServoComponent.qml create mode 100644 src/QmlControls/ServoOutputMonitorController.cc create mode 100644 src/QmlControls/ServoOutputMonitorController.h diff --git a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc index 8c742440a015..be615cf0858d 100644 --- a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc +++ b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc @@ -5,6 +5,7 @@ #include "APMHeliComponent.h" #include "APMLightsComponent.h" #include "APMMotorComponent.h" +#include "APMServoComponent.h" #include "APMPowerComponent.h" #include "APMRadioComponent.h" #include "APMRemoteSupportComponent.h" @@ -83,6 +84,12 @@ const QVariantList &APMAutoPilotPlugin::vehicleComponents() _components.append(QVariant::fromValue(qobject_cast(_motorComponent))); } + if (_vehicle->parameterManager()->parameterExists(-1, QStringLiteral("SERVO1_MIN"))) { + _servoComponent = new APMServoComponent(_vehicle, this); + _servoComponent->setupTriggerSignals(); + _components.append(QVariant::fromValue(qobject_cast(_servoComponent))); + } + _safetyComponent = new APMSafetyComponent(_vehicle, this); _safetyComponent->setupTriggerSignals(); _components.append(QVariant::fromValue(qobject_cast(_safetyComponent))); diff --git a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.h b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.h index bfeda75863c3..f1d1c51a8dd9 100644 --- a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.h +++ b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.h @@ -14,6 +14,7 @@ class APMMotorComponent; class APMGimbalComponent; class APMLightsComponent; class APMSubFrameComponent; +class APMServoComponent; class ESP8266Component; class APMHeliComponent; class APMRemoteSupportComponent; @@ -43,6 +44,7 @@ class APMAutoPilotPlugin : public AutoPilotPlugin APMLightsComponent *_lightsComponent = nullptr; APMSubFrameComponent *_subFrameComponent = nullptr; APMFlightModesComponent *_flightModesComponent = nullptr; + APMServoComponent *_servoComponent = nullptr; APMPowerComponent *_powerComponent = nullptr; APMMotorComponent *_motorComponent = nullptr; APMRadioComponent *_radioComponent = nullptr; diff --git a/src/AutoPilotPlugins/APM/APMServoComponent.cc b/src/AutoPilotPlugins/APM/APMServoComponent.cc new file mode 100644 index 000000000000..2b0601479fdc --- /dev/null +++ b/src/AutoPilotPlugins/APM/APMServoComponent.cc @@ -0,0 +1,11 @@ +#include "APMServoComponent.h" + +APMServoComponent::APMServoComponent(Vehicle *vehicle, AutoPilotPlugin *autopilot, QObject *parent) + : VehicleComponent(vehicle, autopilot, AutoPilotPlugin::UnknownVehicleComponent, parent) +{ +} + +QUrl APMServoComponent::setupSource() const +{ + return QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AutoPilotPlugins/APM/APMServoComponent.qml")); +} diff --git a/src/AutoPilotPlugins/APM/APMServoComponent.h b/src/AutoPilotPlugins/APM/APMServoComponent.h new file mode 100644 index 000000000000..b3a43d6e2289 --- /dev/null +++ b/src/AutoPilotPlugins/APM/APMServoComponent.h @@ -0,0 +1,24 @@ +#pragma once + +#include "VehicleComponent.h" + +class APMServoComponent : public VehicleComponent +{ + Q_OBJECT + +public: + explicit APMServoComponent(Vehicle *vehicle, AutoPilotPlugin *autopilot, QObject *parent = nullptr); + + // VehicleComponent overrides + QString name() const final { return _name; } + QString description() const final { return tr("Configure servo PWM limits, trim, direction, and function."); } + QString iconResource() const final { return QStringLiteral("/qmlimages/MotorComponentIcon.svg"); } + bool requiresSetup() const final { return false; } + bool setupComplete() const final { return true; } + QUrl setupSource() const final; + QUrl summaryQmlSource() const final { return QUrl(); } + QStringList setupCompleteChangedTriggerList() const final { return QStringList(); } + +private: + const QString _name = tr("Servo Outputs"); +}; diff --git a/src/AutoPilotPlugins/APM/APMServoComponent.qml b/src/AutoPilotPlugins/APM/APMServoComponent.qml new file mode 100644 index 000000000000..27285c506dba --- /dev/null +++ b/src/AutoPilotPlugins/APM/APMServoComponent.qml @@ -0,0 +1,398 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls + +SetupPage { + id: servoPage + pageComponent: pageComponent + showAdvanced: false + + readonly property int _maxServos: 16 + readonly property real _margins: ScreenTools.defaultFontPixelHeight * 0.5 + + FactPanelController { + id: controller + } + + ServoOutputMonitorController { + id: servoMonitor + } + + QGCPalette { + id: qgcPal + colorGroupEnabled: true + } + + // Keep the "Position (us)" bar a stable width so the table doesn't reflow. + readonly property int _positionBarWidth: ScreenTools.defaultFontPixelWidth * 10 + + function getFact(param) { + return controller.parameterExists(-1, param) + ? controller.getParameterFact(-1, param, false) + : null + } + + function servoExists(n) { + return controller.parameterExists(-1, "SERVO" + n + "_FUNCTION") + } + + Component { + id: pageComponent + + Column { + width: availableWidth + spacing: _margins + + QGCLabel { + text: qsTr("Configure ArduPilot servo outputs.") + wrapMode: Text.WordWrap + width: parent.width + } + + QGCGroupBox { + title: qsTr("Servo Outputs") + + GridLayout { + columns: 7 + rowSpacing: ScreenTools.defaultFontPixelHeight * 0.3 + columnSpacing: ScreenTools.defaultFontPixelWidth * 2 + + // --- Headers (all explicitly in row 0) ----------------- + QGCLabel { text: qsTr("Servo"); font.bold: true; Layout.row: 0; Layout.column: 0 } + QGCLabel { + text: qsTr("Position") + font.bold: true + Layout.row: 0 + Layout.column: 1 + Layout.preferredWidth: _positionBarWidth + Layout.fillWidth: false + } + QGCLabel { text: qsTr("Function"); font.bold: true; Layout.row: 0; Layout.column: 2 } + QGCLabel { text: qsTr("Min"); font.bold: true; Layout.row: 0; Layout.column: 3 } + QGCLabel { text: qsTr("Trim"); font.bold: true; Layout.row: 0; Layout.column: 4 } + QGCLabel { text: qsTr("Max"); font.bold: true; Layout.row: 0; Layout.column: 5 } + QGCLabel { text: qsTr("Reversed"); font.bold: true; Layout.row: 0; Layout.column: 6 } + + // --- Column 0: Servo number ---------------------------- + Repeater { + model: _maxServos + QGCLabel { + text: index + 1 + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 0 + } + } + + // --- Column 1: Position -------------------------------- + Repeater { + id: positionRepeater + model: _maxServos + Item { + readonly property int _servoIndex: index + 1 + readonly property var _minFact: getFact("SERVO" + _servoIndex + "_MIN") + readonly property var _maxFact: getFact("SERVO" + _servoIndex + "_MAX") + + property int pwmValue: servoMonitor.servoValue(index) + readonly property double _rawValue: pwmValue >= 0 ? pwmValue : NaN + readonly property double _minValue: _minFact ? _minFact.value : NaN + readonly property double _maxValue: _maxFact ? _maxFact.value : NaN + + readonly property double _range: _maxValue - _minValue + readonly property double _ratio: (_range > 0 && !isNaN(_rawValue) && !isNaN(_minValue)) + ? Math.max(0, Math.min(1, (_rawValue - _minValue) / _range)) + : 0 + + height: ScreenTools.defaultFontPixelHeight * 0.95 + Layout.preferredWidth: _positionBarWidth + Layout.fillWidth: false + width: _positionBarWidth + visible: servoExists(_servoIndex) + Layout.row: index + 1 + Layout.column: 1 + + readonly property color _trackColor: qgcPal.colorGrey + // Use a themed accent so the fill is always clearly distinct from the track + readonly property color _progressColor: qgcPal.colorGreen + + readonly property bool _hasValidValue: pwmValue >= 0 && !isNaN(_rawValue) && !isNaN(_minValue) && !isNaN(_maxValue) && _range > 0 + // Use the delegate width (cell width), not `parent.width` (Repeater/GridLayout width). + readonly property real _progressWidth: _hasValidValue ? Math.max(0, _ratio * width) : 0 + + // Keep the delegate's implicit width stable so GridLayout doesn't reflow + // based on changing label text lengths. + implicitWidth: 0 + + Rectangle { + anchors.fill: parent + id: track + color: _trackColor + opacity: 0.45 + border.width: 1 + border.color: qgcPal.text + radius: ScreenTools.defaultBorderRadius + } + + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + width: _progressWidth + color: _progressColor + radius: ScreenTools.defaultBorderRadius + } + + QGCLabel { + id: valueLabel + z: 1 + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: _hasValidValue ? Math.round(_rawValue) : "-" + font.bold: true + color: qgcPal.text + width: parent.width + } + } + } + + Connections { + target: servoMonitor + function onServoValueChanged(servo, pwmValue) { + const item = positionRepeater.itemAt(servo) + if (item) { + item.pwmValue = pwmValue + } + } + } + + // --- Column 2: Function -------------------------------- + Repeater { + model: _maxServos + FactComboBox { + fact: getFact("SERVO" + (index + 1) + "_FUNCTION") + indexModel: false + sizeToContents: true + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 2 + } + } + + // --- Column 3: Min --------------------------------------- + Repeater { + model: _maxServos + RowLayout { + spacing: ScreenTools.defaultFontPixelWidth + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 3 + + readonly property var spFact: getFact("SERVO" + (index + 1) + "_MIN") + property int _repeatDir: 0 + + Timer { + id: repeatInitial + interval: 350 + repeat: false + running: false + onTriggered: repeatTimer.start() + } + + Timer { + id: repeatTimer + interval: 80 + repeat: true + running: false + onTriggered: if (spFact && _repeatDir !== 0) { spFact.value += _repeatDir } + } + + QGCButton { + text: "-" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = -1 + spFact.value -= 1 + repeatInitial.start() + } + onReleased: { + _repeatDir = 0 + repeatInitial.stop() + repeatTimer.stop() + } + } + FactTextField { fact: spFact; showUnits: false; Layout.fillWidth: true } + QGCButton { + text: "+" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = 1 + spFact.value += 1 + repeatInitial.start() + } + onReleased: { + _repeatDir = 0 + repeatInitial.stop() + repeatTimer.stop() + } + } + } + } + + // --- Column 4: Trim -------------------------------------- + Repeater { + model: _maxServos + RowLayout { + spacing: ScreenTools.defaultFontPixelWidth + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 4 + + readonly property var spFact: getFact("SERVO" + (index + 1) + "_TRIM") + property int _repeatDir: 0 + + Timer { + id: repeatInitialTrim + interval: 350 + repeat: false + running: false + onTriggered: repeatTimerTrim.start() + } + + Timer { + id: repeatTimerTrim + interval: 80 + repeat: true + running: false + onTriggered: if (spFact && _repeatDir !== 0) { spFact.value += _repeatDir } + } + + QGCButton { + text: "-" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = -1 + spFact.value -= 1 + repeatInitialTrim.start() + } + onReleased: { + _repeatDir = 0 + repeatInitialTrim.stop() + repeatTimerTrim.stop() + } + } + FactTextField { fact: spFact; showUnits: false; Layout.fillWidth: true } + QGCButton { + text: "+" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = 1 + spFact.value += 1 + repeatInitialTrim.start() + } + onReleased: { + _repeatDir = 0 + repeatInitialTrim.stop() + repeatTimerTrim.stop() + } + } + } + } + + // --- Column 5: Max --------------------------------------- + Repeater { + model: _maxServos + RowLayout { + spacing: ScreenTools.defaultFontPixelWidth + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 5 + + readonly property var spFact: getFact("SERVO" + (index + 1) + "_MAX") + property int _repeatDir: 0 + + Timer { + id: repeatInitialMax + interval: 350 + repeat: false + running: false + onTriggered: repeatTimerMax.start() + } + + Timer { + id: repeatTimerMax + interval: 80 + repeat: true + running: false + onTriggered: if (spFact && _repeatDir !== 0) { spFact.value += _repeatDir } + } + + QGCButton { + text: "-" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = -1 + spFact.value -= 1 + repeatInitialMax.start() + } + onReleased: { + _repeatDir = 0 + repeatInitialMax.stop() + repeatTimerMax.stop() + } + } + FactTextField { fact: spFact; showUnits: false; Layout.fillWidth: true } + QGCButton { + text: "+" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = 1 + spFact.value += 1 + repeatInitialMax.start() + } + onReleased: { + _repeatDir = 0 + repeatInitialMax.stop() + repeatTimerMax.stop() + } + } + } + } + + // --- Column 6: Reversed --------------------------------- + Repeater { + model: _maxServos + FactCheckBox { + fact: getFact("SERVO" + (index + 1) + "_REVERSED") + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 6 + } + } + } + } + } + } +} diff --git a/src/AutoPilotPlugins/APM/CMakeLists.txt b/src/AutoPilotPlugins/APM/CMakeLists.txt index 7ce30f3f54dc..b213f76dc835 100644 --- a/src/AutoPilotPlugins/APM/CMakeLists.txt +++ b/src/AutoPilotPlugins/APM/CMakeLists.txt @@ -27,6 +27,8 @@ target_sources(${CMAKE_PROJECT_NAME} APMLightsComponent.h APMMotorComponent.cc APMMotorComponent.h + APMServoComponent.cc + APMServoComponent.h APMPowerComponent.cc APMPowerComponent.h APMRadioComponent.cc @@ -72,6 +74,7 @@ qt_add_qml_module(AutoPilotPluginsAPMModule APMLightsComponent.qml APMLightsComponentSummary.qml APMMotorComponent.qml + APMServoComponent.qml APMNotSupported.qml APMPowerComponent.qml APMPowerComponentSummary.qml diff --git a/src/QmlControls/CMakeLists.txt b/src/QmlControls/CMakeLists.txt index 019dc7a481c5..564a06c379ab 100644 --- a/src/QmlControls/CMakeLists.txt +++ b/src/QmlControls/CMakeLists.txt @@ -66,6 +66,8 @@ target_sources(${CMAKE_PROJECT_NAME} ToolStripActionList.h TransformPositionController.cc TransformPositionController.h + ServoOutputMonitorController.cc + ServoOutputMonitorController.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/QmlControls/ServoOutputMonitorController.cc b/src/QmlControls/ServoOutputMonitorController.cc new file mode 100644 index 000000000000..530230cc6c42 --- /dev/null +++ b/src/QmlControls/ServoOutputMonitorController.cc @@ -0,0 +1,41 @@ +#include "ServoOutputMonitorController.h" + +#include "QGCLoggingCategory.h" +#include "Vehicle.h" + +QGC_LOGGING_CATEGORY(ServoOutputMonitorControllerLog, "QMLControls.ServoOutputMonitorController") + +ServoOutputMonitorController::ServoOutputMonitorController(QObject *parent) + : FactPanelController(parent) +{ + (void) connect(_vehicle, &Vehicle::servoOutputsChanged, this, &ServoOutputMonitorController::servoValuesChanged); +} + +ServoOutputMonitorController::~ServoOutputMonitorController() +{ +} + +int ServoOutputMonitorController::servoValue(int servoIndex) const +{ + if (servoIndex >= 0 && servoIndex < _servoValues.size()) { + return _servoValues[servoIndex]; + } + + return -1; +} + +void ServoOutputMonitorController::servoValuesChanged(QVector pwmValues) +{ + const int servoCount = pwmValues.size(); + + _servoValues = pwmValues; + + if (_servoCount != servoCount) { + _servoCount = servoCount; + emit servoCountChanged(_servoCount); + } + + for (int servo = 0; servo < servoCount; servo++) { + emit servoValueChanged(servo, pwmValues[servo]); + } +} diff --git a/src/QmlControls/ServoOutputMonitorController.h b/src/QmlControls/ServoOutputMonitorController.h new file mode 100644 index 000000000000..3d051d6905c8 --- /dev/null +++ b/src/QmlControls/ServoOutputMonitorController.h @@ -0,0 +1,35 @@ +#pragma once + +#include "FactPanelController.h" + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(ServoOutputMonitorControllerLog) + +class ServoOutputMonitorController : public FactPanelController +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(int servoCount READ servoCount NOTIFY servoCountChanged) + +public: + explicit ServoOutputMonitorController(QObject *parent = nullptr); + ~ServoOutputMonitorController(); + + int servoCount() const { return _servoCount; } + + /// Servo index is 0..15 (SERVO1..SERVO16) + Q_INVOKABLE int servoValue(int servoIndex) const; + +signals: + void servoCountChanged(int servoCount); + void servoValueChanged(int servo, int pwmValue); + +private slots: + void servoValuesChanged(QVector pwmValues); + +private: + int _servoCount = 0; + QVector _servoValues = QVector(16, -1); +}; diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index 42f446b81a00..a542f6eea7cd 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -591,6 +591,31 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes case MAVLINK_MSG_ID_RC_CHANNELS: _handleRCChannels(message); break; + case MAVLINK_MSG_ID_SERVO_OUTPUT_RAW: + { + mavlink_servo_output_raw_t servoOutputRaw; + mavlink_msg_servo_output_raw_decode(&message, &servoOutputRaw); + + const int baseIndex = static_cast(servoOutputRaw.port) * 4; + if (baseIndex >= 0 && baseIndex < _servoOutputRawValues.size()) { + const uint16_t rawValues[4] = { + servoOutputRaw.servo1_raw, + servoOutputRaw.servo2_raw, + servoOutputRaw.servo3_raw, + servoOutputRaw.servo4_raw + }; + + for (int i = 0; i < 4; i++) { + const int servoIndex = baseIndex + i; + if (servoIndex >= 0 && servoIndex < _servoOutputRawValues.size() && rawValues[i] != UINT16_MAX) { + _servoOutputRawValues[servoIndex] = static_cast(rawValues[i]); + } + } + + emit servoOutputsChanged(_servoOutputRawValues); + } + } + break; case MAVLINK_MSG_ID_BATTERY_STATUS: _handleBatteryStatus(message); break; diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index f7e50ec544b1..bf8771b6bcf1 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -845,6 +845,10 @@ public slots: /// @param channelValues The current values for rc channels void rcChannelsChanged(QVector channelValues); + /// New SERVO output values coming from SERVO_OUTPUT_RAW message + /// @param servoValues The current servo output values in microseconds (0-15 -> SERVO1..SERVO16). Invalid values are -1. + void servoOutputsChanged(QVector servoValues); + /// Remote control RSSI changed (0% - 100%) void remoteControlRSSIChanged (uint8_t rssi); @@ -1242,6 +1246,9 @@ void _activeVehicleChanged (Vehicle* newActiveVehicle); VehicleRPMFactGroup* _rpmFactGroup = nullptr; TerrainFactGroup* _terrainFactGroup = nullptr; + // Live SERVO_OUTPUT_RAW values (microseconds). Indexed 0..15 -> SERVO1..SERVO16. + QVector _servoOutputRawValues = QVector(16, -1); + // Dynamic FactGroups BatteryFactGroupListModel* _batteryFactGroupListModel = nullptr; EscStatusFactGroupListModel* _escStatusFactGroupListModel = nullptr; From ad6539c4da3d638d87630c92a73a2ebd7d187763 Mon Sep 17 00:00:00 2001 From: Ahmad Wael Date: Thu, 19 Mar 2026 23:18:35 +0200 Subject: [PATCH 119/119] Vehicle: map SERVO_OUTPUT_RAW servo1..servo16 directly --- src/Vehicle/Vehicle.cc | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index a542f6eea7cd..1a836bc0e684 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -596,24 +596,31 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes mavlink_servo_output_raw_t servoOutputRaw; mavlink_msg_servo_output_raw_decode(&message, &servoOutputRaw); - const int baseIndex = static_cast(servoOutputRaw.port) * 4; - if (baseIndex >= 0 && baseIndex < _servoOutputRawValues.size()) { - const uint16_t rawValues[4] = { - servoOutputRaw.servo1_raw, - servoOutputRaw.servo2_raw, - servoOutputRaw.servo3_raw, - servoOutputRaw.servo4_raw - }; - - for (int i = 0; i < 4; i++) { - const int servoIndex = baseIndex + i; - if (servoIndex >= 0 && servoIndex < _servoOutputRawValues.size() && rawValues[i] != UINT16_MAX) { - _servoOutputRawValues[servoIndex] = static_cast(rawValues[i]); - } - } + // ArduPilot commonly publishes servo1_raw..servo16_raw in a single packet (port may remain 0). + const uint16_t rawValues[16] = { + servoOutputRaw.servo1_raw, + servoOutputRaw.servo2_raw, + servoOutputRaw.servo3_raw, + servoOutputRaw.servo4_raw, + servoOutputRaw.servo5_raw, + servoOutputRaw.servo6_raw, + servoOutputRaw.servo7_raw, + servoOutputRaw.servo8_raw, + servoOutputRaw.servo9_raw, + servoOutputRaw.servo10_raw, + servoOutputRaw.servo11_raw, + servoOutputRaw.servo12_raw, + servoOutputRaw.servo13_raw, + servoOutputRaw.servo14_raw, + servoOutputRaw.servo15_raw, + servoOutputRaw.servo16_raw + }; - emit servoOutputsChanged(_servoOutputRawValues); + for (int servoIndex = 0; servoIndex < _servoOutputRawValues.size() && servoIndex < 16; servoIndex++) { + _servoOutputRawValues[servoIndex] = (rawValues[servoIndex] == UINT16_MAX) ? -1 : static_cast(rawValues[servoIndex]); } + + emit servoOutputsChanged(_servoOutputRawValues); } break; case MAVLINK_MSG_ID_BATTERY_STATUS:

    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

  • @Bu}-)JJYEQq_*Eb;DI2f_4e7jVPYim-RTj;1C|UKlze`s9@+YM zF3TATaoi4FYPTM}zrd1@3>tA7?q&X=h`B!nyF~!W;^8pG^aXhonl+lo;!(&@+%UC5k>lW zf%_;NTz&8w8U;oq+@Gykd)+y*nhzR;13Mep+haXUAfS&tVWQ&q0(bXKPsr=Z+bEa= z;K8OkA@(Zlb2$fm5cp7R;O;)NIT~M$+Uw=|IBgZ5Ogkc>&|+~Q#_LIVE&tb@l`r2{ zB9xSd-1E9&e7R)nGj@L+uyOhwHjNY7xQlJMy@lzqZ-2fvo;R2sF1Jaq zf+c9{9IU@X=&B&8nr*9G*bQ1#=??p>lzn}qzZ6wDs`Szf8tNVY9C<>9s|sO7rLqnV z@3^bMmL?=ZP~K6?Z}s7Htm8uh$H)v?EDj@sE!`Ellp@wk_gP`)vFPFQkMI5>Fvmm) z^<%EKB2#lWw)m?(__kkkZ@nX3`R(#uRV8{=ts#DC`uSRXOOZYaZE zSIdkb#!GxuAOQ#MS<$_T;8%L1P644|Qfin>6_$ZTPny_Ufyxo&tZ~1$X(T4_5YRc% z>)JG);^y}i$-F+B@}!dFSgtk#O_N2;yYYBDZlBC`4#GJ=%SXbdMQz7qyctE2?%`$& z#6V{1Fthj!39R{wInm=t9-&Q<+xE;jCMl;G57ku;E0`lEJACQCHoEma(XH{{-VNa~hR}A=%K!UBNhwBp zkv0JnyWKqBqr~wXeDGdP9E-;7$>M}>A= zc$VH&pCuXr1ZAX0A_gmrN1Ok`CTVz6nScKK%(G}qX*6{3d!`qq6)dp|2uHEp>>F2_ z`0(s66z#a}wwgDFkM2$VZ1=2LUyVoLH+>yLk`(g zTr1>uU_`=XygG>OTzek@d1wl}Z|XW1>#~3#%lGDy=a`$5jpOI1r z8X!Oqm};{TYn!`UbZ+i%=W~IlIJSZ0v(Jo=?>`JaH)06E*!9iq_u199BCDM%eJtsS zj3Oj~QJDANibsbDTBBj*--Z^_j^<|P>znIV4Y7a!^8$<{S|gqg|1m>9JOL{9=i(|Mzl>A3w5|MjS*DX9IGXpNyaYe-;_Ch}| zMDx9C4d&vHkhH9voV3Nei-pxs-W&3Ol$4Z)IsJq-ieNDVmyp%AYuS@Gx-a%=g~IvY z0Jmp&-JB>81Q5O%QC&S&hkN7t*{u7wfSZ`L8+@#yyx4lBY&F^e=rZ{ zDnFL_dU$Lg^!x-|k8Rw&$D;;_Fd2DF3O%o;}C{|GKfcpF9=t_+3UT4wchONZD9oLs9W8M znXD;p$tL;rtogp-Y%V9;Y{Zu)niXUBSNyNaP*@To*$NMiCTnLmQ9o z0`5dPC|eM0zD=^W{{{PlH)>=A|~AN z{B#e#xH+`|ag>20Qc1VL;O)cS)f@@GWVd%KMKF3ivuj*lAXT3a@CalLF`wwyKiJ+b zG}!B%92vm@;>(4;14!SifcqfPsbH+g;VrfUghewGD4Nr?8Of`zbX=Zk07CidmW@@x zNVKk$6kebGrGNCi}Fg<~Ov}Nxz;qobaRp*|{;wR(GC# zYs<-AL9)S|Xrf<3oYIriMj>bbF2ttUn6O=w&CPGh}C@hE(E@s|BRE>2o2B zPRbt>x?KUQU34J2TI;V_0g3=YM|PqQLmle~yuK|ZcokA*XwD$~K?-39ld4#?8G9%5 zgQnhm+>ozq(0|T%A?GFk=hqtdH<%be6P(pTslu@ZDJQuACrx;&_cD9y$7EOixZu+I^@mFHF? zqT(xmQlom<6wT*$ptr_hh@0-Cw?BBC!a4>1?=P}f`_p}4V0l^Kb4OF;-lm2WFuv>d z>YCQ~cpydZQf%#s3%cBNjMsOv5l*PWZrnr!WH8&=oZd8}Y<&M7kUXmA#B}x|EO)RJ zr@L<`7WuP^BD+#|`BK`$^PN`>u&MiFe;dfbbfOt$ufeYCG8(x6cqGl+5woA z>eIqhB$+EQW3VyCgRdk&-G${$;_1o#RFTzIkL8yJ>Dm|hgp#Em!hxPBCj$dyd?_-K zdxwX;>sFPfW&NCEXM2_-CU$G{tE{n-dX)peOz(QakJA=iTh!RUUaoJ3$ELaj1dMz~ zR-&aY+E;fffd6a0OS|3Lf@Qing`0iI3CJrOWgVrq&rAN@7IHv(m8+i`fm-@)@#Rk! zDwquO#p1VddaANyZLT{8g750nnc3M$n}dfZvQ7x;L3l+1gs0si-|^s!U$3i)kl};5 z&q>-QfsdDY4aa`W1v3RO$%m7^BHO1ejpxj?uKU&F^u_%xur`MZyVZPPXh^{STey*< z%>y4P7qJblL58AcU%_@-{@)rcT8}@lu>B>1JBL!Ci)JeliAmE3dP7I$trI|6VRSOitBqKQSL|9brcs!+srf~#h#92crtWNqzDuaOwj zq5+_!={{-kXR&;pEkTrDqllMBAI|z=YY7+dGl(ain^=XvGZ<47gL3=?J+VgFKt_7l z(;zEzmI@}#Z~YoEqzqN~4U|EFia$kF1tAeNXZRwml>hU3H7TEKI7Y_;UsRpK^~KM1#W84-e0jFRi1pZe|dVFcHC>g)8i&rm_AJG08 z<~+{2?G3moTf2Em75&!!`MN5E?cQi-SP?0(-?yzj9j#S0bl&b{{0Ei2#tJ;aVj$&i zkvS}UCQM_KDph%=+Fs|WHuhA=xalrq;NCZ=^2d#vg?UqQxFLt^#W$dv*Py}+Bur;6 z*YRT<4`cmO+5`7?yKVZaQ=&GBl%ix-1SO0_o{2}zCJk6?tIi1b!HoOjd5NtZnvowI zip?4Sow_Bns!Y#|PCP=Hsy%6o7t+9oUX(fgd)^Sh%10b;M%nQJ@S)O8QE>+{&*f9= zYMWm5F0BBBPA~hW?#vrt0jCu`8e%Osj9N^krV3;GzMs8Q6Uxr&`y^7hWio~fC=t;? z4WAG%`eBV-$5R(^V?)q?xZ|9*UL&$hN)HLsLQRT)j6({{JwCFZgzatFvpVP=tYI=aVusiFcRxn<}065~UT zLmI!A13v4&Edkq>P0bkTfH+t#8y5#QS~|0-$imMhf+GRK$^{jze&e=-77iB}Jlg3i zr~rCvg4~BO$aK-nJayXo!DjyNk2yMCyZj4EQQ7n5BIcAexiymS;L-h@C}x=Ej$jz8 zYRIrxw#0w>2#{AbGm;-%&_}e%Y~)}ATIuh~7Xplt*4s30AxK3({p=I0k93m*D|Zp@9| zC7ET^y$s?PONlv~D6sgu3@cEy+}!b^#rY6c&%q9eMrL zW@Xg>uk4~YJ%JHX-h67>7>QC+Ymt_#5Nb%Yu=Ni7kmNSM-jTqeRNw6tN}Z2r|Ib$x zj!pae_D9H}Y(HBYwXU>Yq&!TX3e71RP6c(|)NBI7Llr*Mi^k7rDZs~%{vfbpkXL~_yon0N04K67g+WZ)ea=C}~ zvXnW}EB?!$BbD$0_LU9P@u9^MIvjdE;-uRvvb>oEws(f#f`&7STs(6fC|j(~1E@L4 z>r5Mch(5`7R?n%3v(Wb2GVR!otHY;>Dqe86>1%-2)y8|4I1Bk>`bn{MPIL94VSluz zVcPH?WyVd~FB_K~ISYbJW(?b`g!=Z+!>mMG5tCpt>ibD0JrU+gXR4Ix+ZXE7=zhvz zXqUePG08Sw+EWOW)X-)xJYst5@l9Y!>HTPBJyG*RqMw*E^zl<+B&C@jnCT*3 zR)M{fHpuq3B$MY!r+^;kFSBvlt5@Imu_YO}1ZzKBO&m;5PhJ?cvMy@UR(P~Bgrt{J z6WpsMfT7By{Upme);n_q-@j{slc~_Zm7UFCGh#Df&Ew;gqB&uSEoe?tt`NH7p^XC{ zDKosPLeMn5Tf>yQdpx#qTDqwU*3)d`6$tPt3mCyNypsk$k2U`SS8yfAyoY>$Zwg+m z!P)YZ72uAcY>>&u7Qdk=4X6SCy>+m}{qxH}=kB=lV@I#G`zyld74*)jUn zX}e{}B_ltCDuEt{VCEYWW9SY_o!OcbrGR1=^RyFOoHm8);Z z4sdnK$ZT8reaLLVefC?f-P;$TKu9M}6hHqd5P2~tk7*T?tGGjM&~e}ue?DDda2B>> z6BB48CM`)uPmeftdSoOrqW$_07U1bZ<7ox{$M1vz_*GKdNs&DkyOuL0Dsh^`dcxRK zL)@WUi!Bb;>b8l^yS2cm+OKe2v>wFTi;aOVBKHNu5-AR6YD)J~YJ}h$LklX$J zZ&Z#c%*C8|xGf~Ow!A;#iQgk5-?T^B>2;^+yaN+Kpo-OWkRndqBiFVX?KJH~9WU0X zVVX~OAmFNG$2KB?X1Bk^>fM-=^|#Te@IKAwLyc!tX-gA(;k!dmI9>{_zh^*@GCE6p z{HHIsf4K3M)nCnI0zLDH7JrJ^$wpvD9eIj@tlfY$!cgJ970ej#thCuYs8f>!Q2yO% zo&gh`M<%MwibGRP$i$v4tOpiq?9E-7@n=;i1D|iLHR*fod;>w$zzAfSWHP1efd$xc zbEYcNa2#+W4W28vfFPR0M*dA}NLlUV=QDa8FL9wpq{8Ob?w*z*S3Q2mgndN&CVft- zVD)LGcgotR`Ue(@#ubvE^*^slmTFq-R4P~w*ex2Amdj^o`))P}r571gQk1nWI%umP zr^bX4q67YR?&a6hYrreBD`W!{J*;s2+5ucRv#%D{ zNB1C%yWHmeypCHBkQKs9pJp44l~gtoF9vBoH{aLQMZu;N38mEGCmfudnwS5K93%QF zhFrX(*CM%rk!aH5rKfDawoKgUV!M!Crj$IH!Wv5KY2jZNBEEz5FOd+Cw1!myBXvEw zHUbCBFJHdk(HDkAgmt2#4?jFc{KfC%eA2fLWi+CV*!vKxqXHv=;(G#JSn|9TQ(+9q zt#u*N21*hW0mCmUbLc6L%RJxx+DpTUi3$74LK_d8Sws7G>UyzXZbj~8 zHrV6s*i`^!EfEzF;bzD*;M`q^*B`#UJma$Y&C3y|1;J8}5@)BSi{2M+i6;jJ7JnG3 zXn-jwCc2ybuDO5`z8CC#Lx%77sUs%GfUv;rr1yY}K23XnPnR781qB6r;pmvva^t8x zNjok`v}*~~naS%_m^mb**I3ShQFatz!%AsSclRyb z_vL!*k9F1>-G8_R97RP$_LHNa8tFylvvg7cYH{V|l}b*_U-h0ZHp2n><@uRzYZL&D zoNSTvoxj0UQ3JhjPyf={J7X#}eDJ!Sa9^)NRKWyf$y`>TCA-RoPSkzS5c$NzE2OcLOOpheI*WPO zP@=4kXkFRPC5Pzh@wF{Dsjz35q5W`NR{L^~sBCoXx)}WL0k#L;N9;X4rs$gO_i z)O)(RqOK!P$B?VNNk+c9GQi)87xY^sk!-J&ykq=g=Y>NC^RNJVBw9@-NXXd$q-mKa zJ}oOd+q`!~A#D2K6W~O(>q3>iu=>k3vRE3awG_i<@D(1b9I8B@PYIu^kyLNrgGwq6 zzkhByIIfp zo}P8+5~bYx+8a%BS|3DbJj5-ETLJ}q{Em%9HjGJNgRHRp8gm)m<18(dX1;Q7y%EXI zJCbtrQJT;$p!+9JPB&fm@;^5W+#k-U=r%IB4EpOB0JdX+qTp5DV6Hmc-b)tTZMJGg z`%rI~7+N3T+k~U6OkSf$=nHfUF>ZP?B^^~>qG8<&*fEW`oi-1~J*!LO&5GlNiVWo} z(aIpjicMjBy)G2@6?E8U3eDTWw}k59`k=4 z?*fz`DCs?@>kw)eOoI-kZRrh`s|`kUR^R!zM1`5+GV-fZ2H{YZpt(o2OV3Al@}3*# z=|vKrWR&9U5?*g$K)HQR){*CsoH8cBPo8KR^IzCbJq5yVfzpGSd80DB-U!)nn|>(( z2fQo&G<`SVa%F8QsQdkwvz1E5J|NcLn|P&w)F)bCno=0Pa?b9rK+Ny1_-uDLpKfZp ziDNx$-H+tVy;7KhltUL@K()VN%M0c>TYhizG3GBfTM%%}##m$gE4Z_XR+l;KKK=8f zCIEyU=L<}U+~si<*y4+kW2|Gy&R3XSYQ(63Rr*D!M6*xDdx>9=Ne>Se%q%odwX$n? zk146A^5O2%S7P(gu@$$rh}D5-bysKfjAwCHcDTySk|6`wzaqDb7!*g`mydN?Iv2=j?nG6^49nxS*!?N={D{eys<*)zZD{Y^`X5%xmL21J=doNh&Xg&!v{3a% zryLmJku89DC!Rg2ApDhz6CSN~@Y%9oUbqd`n-~+b_-AX7bqJVlc_1IEuuPmIYJd1w zt~E)cK1)PXY&+?mR(bKr6U4rwJ1!#K-(BCjX;s0hWmmas=aEV$_P*jkRE?;iEk!#o z+su%=(DXgX9O(5H)9ol-ew!gU!P27NNH3!(=4*P|M#RLJol`+_{Ar@wf>N_V{0uyR zvWGjBM$8aH8onsvE)QE7-MAAZTyn`Qe(Y`d47b(QAqL@&VpaEH?^N+hIu0%7RB)=33)CcTD;pVno407!k7IaSQFM9*CKYn3(&F@QRo& z68V@DZI*|{sI|`h`Odk4Hmn!xrIggFT6=f>dj-Sm4~~5sf*kPKH!;=hU8lrB+B?) z+hSk`k}vZGHLTXc{xGNEzk8Qd!U+1tnGxBy z#GR;*@r}tW`d+uRVZ#cFgh_dcOk2YNCnJ-V5Hj$@oHoHf{X&>Cvf)4}`N%V)O>oypCGs`0 zgP;XAqFDT&7L}q+o76(eOw6b(o0Rocl8T}_z-?m)k{rN(E5EGBO;!hB^V ziifV~oS9KZjgw$G*iOV?c6L$=(ujn&&l$256>+R6G-I#t_h-(G^;C@~;8!^f;3s6v zkiMs6HkdI~J_Flr`S`C-BK>wo@K*YcBwj~0I2__}bss6g)!60*Z}d@#<%G{w)9F$kRm5CizK2_bKgV(XcT@ zTEWILdUY8Y>a)T&1%7I0?;0Mr~}4Se8LZX`Rdny=yfQE06r0za(g zL^siHS-Q!%bY8&mdHsc_fq(A;PEu3ZUkE-rK>Ns0DPvE46DO4*{4Z*1fLF0{ywdPWIH8Z#bDnIB?f> z+(duEq9f?DJMA<333+1ra|Z}D8EOxhmhnO0P6V+{Bvl=NqUnOgPfaM2GA%Sxd8bkK zg3_loT6f$C&n!ka)}C~#1Ounxfa(S(K^i`Lim{s}u<<;GUr5^fax!S(;|nPb@bS}$ zV#40XwL9|oPsYaF&ewBCncLP~XWOu;(YIZA#YO*?8Xu7dTp;kd$TqaaF}*tAvb5kopc)GY?te?3uf;5@ltylsa+vr`B*}c{CHG~s{HOp|cJQ7xr%(J%Lnb!NhaKhEc zkwjMDPrEz%qE%V;d0Q! z>Cp`0%fOiLH{gW^G@<-dVh2{HsDggAU|eOSq}f(N8atL+kc1F@kKks;-9t5vY;pd& z5rm{GcUqdxbgQ3il=1_}y*|uL^Jc(v6Vb3l+y0@F9HOvQ_G_W7Zi;O=DV3H%Y=<&S zUg@sv2alSjibvCll9GB~bcBFW_GjHoq=^sSzHG9cnIrCl#P{8tFer0`Xgj^r&{z#A z+bqB2;gj<5)hdrCBJFc3X~487T1bv)twQ;Mska}F(w(n}Os>N^BB8Zn*fH2VNDQ9%`>t97EHdnG1)CJho-%?N1}8-&jh0#MD%C;5CkLD=W~Vhf>o@DXrl*CWhcc-#=^DE5F{m%C=)nSBOTB>EH-sr2~ zjX(yceEaRw7>L6$1W3-=xd!8NzE4hjzqKV+wRvYt5T%;hw5ztY1LxZ`>*ZdN*jCUh z_dUATkPO&EwAg|zYd!#rXZi~mg%jfZJ%eICZVqj*Nslj92vo=BNTN727&aUoc?H~X z@pV1jMW*@RotUX0r`&s^d5<-p13!RZM$CjbATo_SDhZW~pgEjk6eI+%$OF~dWZ+c& z$ct7sIsgp*$V_Zo>-1l4H!(Jz6+OJGOp7EU!RTTVM}yl}yayNPkEsij)F`Qf4Wz;B z0&sDD27SIImWgMD;q0_DiZi%oD+N1mUa7f&}Iivx=YrSsA(Q1Iz zmW5>~)fqL5ZTB+`tp=wtjI8#xR^Zv~XmD|%M1(M?JGo&hz~qBb%F7TPZJ&&c1l38} z-n-K5KfFh|6^*we+W!OMu2z#v&=Sn${Q+8^k?UKFb*t1Rh*Ya_D@c^=r*dEej z^sw{kxk2pdsnW}JTu!Q%ZWT!;g{3z-hDaf$jmJG>uTPcwpxcIdB=r=@Zw0OJkDXaw z<`e$GSaGL!Wmk(1(;mmS#TFC<)K{0ugDLX{Qg6qPmK)}*tu?rP7VzYd2C|KJrx*V$ z07}H@MTRjk*hrb7{6O?AIz4^PrJb>gh@|AI6Ozyt0&KY39 zIw87N6(N~4uL1lN=>j;rv@qmN*VR!;t~mMeg&e=xn;*@WZ);z1f;+SrkVnV#a}-_| zi=84epoj^w0(N^R4^~_5znB~=dHVruf;8DcoU$btK}8B+ZC8+C^*kcr`N`i)N=(en ztCB#P38=M}a$Cr_KFg9oc!q5=235>7N%f!%JCBNDa{cmUsnPL+s$(-7XxoUJs7cDA&ZWNx3{h4J*0e(SE~-5ZepK5fIFYYx{^6v_Kd4z?JBuw(}+N_ zN=|Hcv+c^6+Ecs_{nHQs6TdUgCiDkOE=bF>Z9&a+yn{FQ66Y3{;%b4yAP{B4&+Z?h zlh}VJrY#S7f=KuoKo-Bn*Tc~PAZIGKTQh0GeH5(VUvWJPIzmJ~4U&c~@OUrN=%=C0 z{w{vSKDj1Hud2ckXZE4SPI&fZkpoyDZm9SqQWu!VzS7IhkzgG4L^Ev0m%&uu`^Di4 z!At=_y6{wk_JmiY2t9!*@q2O)%8Lhbs_dsj?EL)0z8Sn6#de>734f6o+=uB)I?c%xC%)BHev6F0=u zO{Qy#p@2Pm=N!Cv9_~%;fTe9{Q76_5SpcYw^O)&>Mt`RiEL#F?a;EnGWNtc@dJ(%~ zN)L#`(*|?osvA0YcSfOUmr*V!+J>fFJ!I4HBG0b>RNzzWkND%^fH0A=ceLN*r->Tl z$RyPk|8NHbQ4o@G6bf0P9P4!CJ&1P%w3OxVtW0W)Lr-~bod~I^+Xi+Cys8nnY(zN@ zUSj0zg>MTd5^9+kzJG2F=Lu6Vrvd1M8a3C1aO4uhvp7nK-xEs@r>QCOz+B`Zg&pJ3 z#K!{G!_o?jAgSOMw^JU$mZo$-YDDPJMTe$(!J<}(KPn#_KBJRTT$@ZImM)th~hu=ac-%dZzMKA+f+JZTiFuxfr z8#)pv5pWYg^}I-g&WNBhC$bcv8kg4v*yxJ5b~A3;_sZ{_ugwA8l$c{S>+F3xWs{<^2$(fF;$}@)@4|fK(a(aylu&oA_U^aqF820 z5ET3z-W5f%tbHT_HJ4bDFyRK3uWhM<W2dyjn8B%(>=a_(*1MCtWp1RSQE**j= zTBAngLWx4s4O_jKnHiryEq?6G(wyh%+DlGN5J8%f{*~Bx^6_&d=H1d%jWf;%uwY3h z2M)3IK0zPk%V3MJcvq0+p9>N^LKLttdQ-LM@R+6f98>c9w8m`m?z2zZVT79Fl-lbQ*6SLSkp~B(UsF{_c$U zeW<}Pk3`5HX*dV-8H zzc8*}BR|G{wC>W~Ml-eTTTk#u6m5ITi3omc5=6b){sd^9kDzafzrF_3{!djK(6Eu+L$Jo1IjQL!AmZ z;U=zG+K!$12+IZjM$oa^No}^Y*LZ5yspR{IIO{y=OcEP(CzQCdsQ#i6l>5o8CZ4xp z;Zd_{L844FkrD5rwv5VeHG+R|zdb&O7^_f){esJxGL(v_OsI6nY z9~S4Cd|yek;U;VM#&Yboms=tFeV$`5l38Ep_a!R4#Qoo*xdMCs1XEhL2A-V!>#8}F z8<=O#sG=AWNa$~H1*AoP-@^+0itRJ@VYEP0p6}o*%BzNgA3ymO_8Gl9njAz5l4u{+ zv8ot0I|zur-=6Nul1y$bc>#or3Z$W|oLJ$f3h-|lepEcq|A(lvj%xA`|Ncz~h{Pt{ zFhEjjG>F6qDFK;?BF*S7X;6gG-60)AKtQBRksKx6Al*ne&%V#^`JLz7JNsvU?3{bM zKUcl4_e;HI>ZDn`p0i zW{s4!7sQb_m#@>`Avy>D1kv@KcDwCL3FoA=&k{^t@yLFqc9dKu_PKdc`N;`i3^IFv zGhIZzQ~JsktE=RJ29C)I1*~U)G5ZDX`G~=XV9S%hMCeYuMvN$FJk#;!9jQhFW>M(I znAh)2Pu7L)(_^o}(w)XuT~E)lPg^CA4w8_WITeI2a_qK)%VXoe*$N>@DM%VoO+myG zVxL2Dzv?_z{w!56CS$<@r$Vy1KJNYX%~tXsD*lfEkpzD^_oLJTG00!s6!gYz(Tl8o zn~#YhiOXzH1?|NoMEv@s7$}Lq?jzr1N~!U-n(62rGeA@1V}EQ{kZ_l3hqxMjZA(f~ zsN~*T(hUr8t?sm#H7Oo`znvI7jv8;?t@x3`(+d&ORfsoN+J#hOij-zf5-R?pw|;SS zWZZ>p)*&W65)tn>#X}o(Ren&9%}+)YreP*`aKDS*Zp4R;tOFcl)|xBZwliDQ)r@VW z;4z}cq-(UBbpDjef7vK)8iSb*cq*SUz*l(kiRlJtPpIL@Z;ac1=h*Ta&E$6kwDVLp z@9q~DE8F*BxbZrq#KEWpSCOLgLlZsBcvo1>5-$Rj5OX>5;P5SeyJ!;++^8~U{A;6^ zN(c2bpBpqoM*sUkuR1!|>gHyNm4?Pn#m^`KJYA?;f0Dj^Kw9kr&>}l76?JeR1HB1PZ@ z;Sv!s<3?f$$}_X*P^Y~i6pb@2)`{og*CXc&H~k%*fD(`A@lbKfbom-@AjI9otC&+y z&o*rp%T7Q^6ig$Go|CGSa$Vdweb(r6Q2YAs&AeZ$E5Fuj?jNuIO}lTGxweiP1z@Dx zhE3OX69?yH*6B&lOQ5c0VFbhEXmZ{0o;K=#`XxR+ zRK`{62?3ti5R8mBs)Y4a3%C+>pfhK6KU1SdYRBQ50v}0Mc)}qY2uJ4})bF&9SRDCP zD8p9YX|#GG=hy9I32bF(liQZ6Dp%LzD9vo?r;t(%s2Uy#u0oO_T3hSrOX0k)}clRNt zaE;Vy3dwj$`aHzmsxcM zjAhU3tENmDZ#PLnGzWb2YdC~CJ=X<*s{8>h+tlI){x)4Mea#`Cme%hj=o5{LRvU9g zD8XGh1=v&iabRu%3`19uO8d_XigfA>d6|eFq!N9%r-RN)Rs|v7&1OIz%g_HPSL636 zt)}jY{Qm0l)p$+u%Z#+9SLpMz0p6$YV(3s>)l$CS0jYM1>xq%YH+7N)+RQu};4CZr;gS#-79ye_+P4-F_4~8WuLh=9%t$Md^{O!`$ z0Ri+yDz)bdGt1g-Q-T#F_pwc)6s%pqSye&oijD7n4!0nR#}siRZd zPgO~~DwK@LG8O+!e(|LpY2EA4W0@{VM?SH1i7Y0AGyLY>*tJf|n)gGr`rFrW5*8$J zIDi{4M^H^e#gi9!J2fLP7@SSZSil%(py>7Mq@YBL=@Gwbmb}u(w2xD*Xt2XgnZee2 z1ABFgX*#ZVKaT;Ib+rBf#tfj+Vb;U{8(JdZv@9l6OS5Yhmdkmj*h$9dPc*pY1{Oe& zlrU<~Dn)Kpb^FI4(|@ps{Oc0O_1{`ci9pU4{}?kiK+pVOj8ftf>0cuTdaPf-6eZ#f zW%<{LVu$9vsDPVgGO^f__k#)qr>0?%i)TwbSZ)|dZ(5~M8)G<9$qKN6c*%U($m~ua z2Aa#L!Jo_YD&PHSEQh-{3O-`-U5Cc7n8G6gW63P$Jf9(E2vBP;RP(9DYhpEXWZZ?( z%=fa82i@FKi{+0ovb8~Qz1gtww4W6g5lAK>s89-5h^JB-5?XXz=_sQW_t#64x^TXj-sst=+7(nf; z+naAyvu@Tj)Z^lZ&FEVE7W<;s@-^+LQT6Llm{`Fu-DUI8S z`rJ)#U}L=VOLiKUK=NP#R1qYWZ9aWa*WE{yn8m>936qFAu99F^X03e&hSUm(r}94| zW`%UF`c)h7^}i_?v#!9lnw_v9lGx}^G5jMsk2!P(Oz&Y*G7^=r5|E7yG070jri3juSUDi_sDEF|*E6 z4&8h8x6f@EW)`7OWn~+#mAj9{)0HI>ze^IblhS~xWe>BzVGo;(CQ9->I7vQ@o^QJM zFnWj&f#2Bar3QV*u-d6N=+-+cRQ5c%u$K_FcKx5s^mNH=IHRwrL(AtoBX`Sh`;SIhp;^3mV7d28?3 zsbXf^{Ox;khTF69YA;K21aOEMK6aTiKbejNoX8_8$dhRrXWnS_OV~0C!c3M9)&q4@aQ*T4pf-niK50!?n+NEe*} zQHCfwZ7eNF_3+p2%-l=|b{tmQn%KWzk%~V>ML>jta!b^xC*yFqtX}g_9QNBka~1KJ zW1<28Xswk-!4_8AA;{RUw0=~4reY*33!bo|Fihx>*5plPzryCizp~XlGL~ShCGuYT zSxUt>JfQ`0(3wTFM3ZlnvtE1*(tZ{sA#IcWdk^EtCyc#w8UoBvg_O~t*mYsTvhMFd zF*S&3qRx0uqAQN6Bs}~)k{8Mayp&TkA0zSNTO5Qlu(YYEMPa&=?(nFnKoxn;^dk3X zeuQ696Dx8OmiuLLin%Rrs9!(E?)I`y;Ud zOvFG^V=Ao9%yHd9j;}dtQ*xnGrU&GY%}&%NlIG5MI%Ug2TfAalCq-yau5`!M(vhle`hSGUtTs~2qICzzDRMbg0TD$UnhNZ4 z8tYVjz4=FxxWkt}Pq?slMZ+xwBfmHloaX$9!LNMcK|ji$K#G9TpBHf<-z2a3wk{Sj zzOtNvDDT-cXVOf5px*gC$YlxTF&j@)IPur?*C|7YFvjS1sAanjqH$Xw%_-y8%yM}k zdDTG|r#6VZFt}eL!Y!^TVzJ1Su`GzlZ4Y7FLOAs@&xnsaG8V{r$KW*Zh%JNhX_y2r zO^668fpO+O_FkxY1qVIUuMU<22ENxSkVP*~eWNqU;*IAbcJ<;6U<~O#Y)bR;Yj6}5 z7r<(7Ph;J_2>fN--umQODIV3$l@bL(>r0Z&?*)mxVG*`8c^T<$hShLfynm|f56KS55 z%XFoJAv&+;M*0SAUU%T+bF}#`H5?VkDBXL|PKnOwzh;3vzToXIvRYqfMAHw<{TbHb zu~8*Qw$m8AfB3uv1!zS&%NS8AVkxO?A{c>!;V5X*;C)q0=6k|n1;VSp<=b^4D$fq^ zb=ix@BpL-hA;pZ=3Ir^%X>oyGgHqO!7K7tca3Bn4eP zuii5~28XnB6>{UkpoJh-(?_O}axR`k5;S?Z`}Ib6SsXO4vuiyRFycyUE zVZr6_)j6bKxkUW&LY?L+$g*XmS#~NQ`@Q9gUdUaWEOzX)mDF{u!mMoj+z{ z&q+S#9AlSL2txIM2M{QSg|`5#!!9HUNW+4a9uFJ|GULWc1Zz!-KuihB#`|h(=FFp! zI(@RQlLakLXBjzUuW6DL<6H+Ni0jP#{jwV3bnG`^ZMIEk?)*@dNh0Cj06u~TvzA%p9U`XRfut{J)*B$@tBMLFI?RlS-FvKpIX?~ z=httg)NZ7>3twimdOaF4W9V-4s&SvXqTCF~BCffk!0GaKx6 z*SWiZKaFzU%Xb!v)DMwk^!V1$tPp^AeyA?X52q{~SPOPs>_dhPqe|#{VZ5XhXM*e6Rc+L6R^-;#?R@Tak zmZ|Cf;R0#GBoyC_7aN~A=x>J$jq(G_VbP6}wXfBZtA!5#zM}|hBnyj-9}!4c|FTme zS3rn-BI?kMsP3~aEk!&nwpcFQv`^q>+?Zot)24W~Avcz+DDJgCCuMj)yz=Ghx_(LG zyRL4*gH`hK2ZG}Ob(+?8I)NE)#@v%7EnW8I`f?KyQWIWt7F;>ZEEa}4-B+z%$_~Lc zlb0o;Z%+5AdLu`gr;YrNaPKnc=zU8EEMZiDZq<(fJ2XO~ZfNp!LGJ2iqb*ja(fgDe zFlcmp2%J&520)?&qd5|=>ODcrT~TyMBo+!)(!~zNP@;OjEP{PPPe@Yn@LP#HnP%(< zTxaV{xG@O#EuuIKKUkMMKdLtoiQ!T0z-KX0^Ym+-& z>u*RQ6B-JbRZNZ9u2{;5o#RWP;BRloU~XoX99U#?u`LN!NK>Xoq;qz}43K3DURqxkOoqX7fk2DVAND?bGf9bmFoyJb zm$fAs58Qf$0yi`zGbcg0!N!QPAMWkb=p0!HAv;SR!gSrE)}=WP*;PZsY}o2Q;LR+w zHV7gB=#5gfFOy=-zB#N?^gvuDpQOTRGZeYqL(4^O*4{&L6PSKW;9 zZH78JWF{6p?UU7QK1!-2Vv+vWNk)VAdP-7NJMh>$(#}liw;}vRe07JFkn^3sYH6%2 zjP%(FD6l-aKarM%^H0|l`%tQvWR}qJ2WcHRnE>?V-KjOjDP#`x(a5tK_tt!N3}Pxm zTV|82Fx5k{7ttm+D#8xIzm>9@A^@wAumq1-!E8n8fMMQk79P=TP4wJU1?D;TU@(!LDk093by8i|ghUQzy@& zn3yFpjvBU#!Kb0T*U>aUY9t57X%6)kP5_E&z~t1-zzdk1E>LzElrjZ%jSoR~S%e`) zX*V+Yjao3nhz5T@>N3)WbNgYQ!`=N|)V3ObO99G&5)jB%4-~KzfwX--;L?|FPvHtf zCMqhzs<^=;7L~r{$%+qweet!(OcX6nV7MA@9xD7}sbpQ)&zwi0Ay51qdPjqT0Rn71 zK4h`SsLr^;pU~IlHfiNNhVsE9=BV^C8{T*#5w)g(Zy=3x^bas`DG(Sntu%OfxzU=3 z0#V0d`YAxkw9WL_WDUH2{rT1h*xl4-LD~fy8K&98Q8t^*(Z(5ZaAces%6ZGj=P(#XOhQ# z@jdEobYSwjmXham!H8;h8q0UQYi&Tzu0JT`rP9?gl1R;DR$ym-9nze;KCdn!#dxx0d z(?b&It7C`*65@ks&w1wWconCTp_`HedLOeS9I1T= z=sF41SPYKw^hb5ed=lu|v^~lByqO@_MG%hg*q@Vt`s~>&0 zr0Pr1;VX1Y)+z`jwgmBs+)TJ%!ZL8swP)o;B)c5Z2zqn7))wI_@P&-Rc$GHB z6oumZM+kHg8Ap5u{W?2CXdm{Dau7oU3+9AgCC~l)^wOU7uW6OljEDSarYpATI>!5g z_3!TpD%Ziqho3iccBk{VhFXy7X1C?kSv9niKMJycTKD>la|N1aGUKJ-s9P}qYb3mIjm+*%fkP2;-p?#F}ivT z_FKNc`BcrzZ`dr(7?ZGC&8vtTTVt-k`UI>k2kqyEts1=9JzXQA%5mI^{O~^+$A1N~ znl)cF?Aat6J%Se`sYgU@g^v@qoWXpJ`HbPe)TCAaSN4L~1qBWLleu9)9s%L+GRl;nifNZMv^5?Klgu=Z0NS$HDKOA@6{ z!19)pP6S4mt3i$3Lwb7{>VMXDl_u7_HsC(%!nrdh${m5w7N9`E;euvPrwW0hG%n+8 z>>rEt6BV_YQ3X9%SlGtB9U`S6gfPf<6n|leSSh6xw+i<$%%1zTxJ!{34JOLBYLj@@$@Zd`vguc?SzkFSj93^BxmQ|aKd zEbOnUE82b*qN9l-gat{EM4uaONR4s%xPE8v+O&MODr&5ecU$E6mEnPF&}H7?E4nkN zo919S6(U47yC__cvT#V$6yf5Cq}f37J31bub*C*y94WNqa-sN7rE(Jv?D0!?dPpXp$VFqGU2odmZT zqz-Y@O-`?YNh4P|aAXu;WqR2~Wtq5ouvRxD^OiZ6BFBmZSmKka-<901T?mFl{ zE~&^2y?5PQUXHl=q%bG0Y9;P#nfl1%`;^zz#$oC5xZEC#ugO8*Xuz4%mtO!QHZ4)Z za;>BT)K#==X%yO-AkuS9Ck%C!B8IXODfAri?58+V>rHUPN>>$R;ftK4RJwjJe16VC zLL}#Sv0dv%Lb9<5`j!$*`b+?Dszy9Ipz=n_|B@F_r`a>nh~fSt7T#4u10q zHfAPtI9;XuE0`>pC{aepj0<+H%*mXm?GvI7#SS#(^n0q*siaF0Z)EXv`40#j2nN%@ z*ktWzXR{p@rg z{pp& z{h`_x+pziH((alvD$u8xV`HkJ;{Apd24bX&g$}32q)pjdZWyLvn@z$eX2T}Syrt|e zVNRhmWMHJ$LsgPkCnjbu_-wabshgn9R+( z^UW7Um^x8uX_2G%Y|~8E{39cOY2#E5>X>dW_7#DQ%s}0b?8BmusRbY|#tG^J^29C} z4>IzPCF>JiRejbbC6rdVJaW(=dAh;JSiHZ4ESLVo>b!Ri%zXDzhv?mssqX5UTs!l} zKkGxjM+qheR~t+L(lJjS!E=$r(OrsmdQWCl=Tn(CQ1qp^;Mc?}rY9@+`^uIHN0qlX zLtG(jw2-oJ9K$(6>70COU`yh=5jBRZ=Xma?I zxQ(XHNG%?qMX4^#bu-Hv7~O3K31?@8;G`3q=MO3lK+O?dL9bNvO4G#f@gqP#ZN`H^ zu^;@;F!fi>0DZE`D?ryvgcX3RYU<3wlc3!ew ztCnIGS?t*Mv}-fEgX4$h;}HySIrNok%(JsMvp*dDW*ZX!^bSP5UK=ZQq}}w-eeqDZG(tnr?tznkQx`(IwJ*xzr(WMwRv~oZn%A% z*I}|)cwAb<8|TfV2)|^80WA;_`WO$*8_$vOLe;uTT!o9`b3b7zO0(Dua4Ju7N8#fD zALB-!Jj9wL<7uS@=kB7lmJENkas8=uyG4NB$h_Q*_!e92lQfAn9 zgLM7K&kRMSjlu;Mznw~~L)-|+8xN!Jw+v;ni~l(r!jsaH3V9CJu*yyspY8m3JC1lA z+~<8u6LZXUN#s(0sD)wTuow3n*P~Q%S#S~D;ike_L=w&Lm5^R*>O&RPP8@10gu4y+ z-%PFTpc@g3dY_>3+!rP*$4AdA^(^#%n3_+CmhX_WBmm&J2P@)7jGzrRl=W-Z>c6PFl->1VO0NKlS5q?k@g`dD(4$TM+qvXgG3G$OOfpt)xL7 ziGkO1b!HT{9S*vgG7!F5QAU1Clo#{D@jii6w*@+%bBf-%ACEuZLq8jV+qlD`jUf= zC7uhlwl^)D_K{Wy^H|9?ul$fwAQ+PN>_D#{!w1v2xVIniH*3LHK!ip0g0tjRn5ALK zuA{-(1Nr>hc^p{t@mW)N9>#22A8=KA4=&;D&!f%IA8e@Dp%e2^)G7IEWdKibMt&14 z(`9$D+V-}z#Mj3B^hC=Y_>5ZPWhs@VD2o&xa`^mQX3*wt^emRKD9u{ts}`Htv)@#P z#_O&dg9Df*iq{bvI7{3a!fp~Ru@%AJsyli5Xs8tcYN8U*&9$|KD2NqetauA6@EVB^ zFg9HgMdgNa?@W-45&@obUO%j92WEsd^p&74=O2bR;Q8Fahsg&Ro%=IPdqBfR=5}lG z@orbK?R%N~zsG>n*@=D|@i!TKBPYGQkqrre=;1U0jZ`)z3+aW=M&sc8CfiTyuv%)W z!Zb||vKn%1_Z2J_fVPP(XXQFHF9ks7l7n%ugV|_0FnuYw$^eOWz4Hnlz(eiKeD?4& zODs#Ejy5@-c`!;cD+=xkhug&Km`>Q}Mwe&}s5Bbz%_MT<$0JQwL48|pb_lN|%Ko*E zg;vyp*VP$y{st7&rUI6(YUwb2X>Ry4*L5POQN5GN5}rmE6quTxeGu5_pj*577|IOE zHtrY1mer&CUZQC3iZ!RMBUFZVYa7@7DZD%dK9sE4et z^9e-eB15z~6_k07UEZ4s;Ks5jo4)tm`q^(XWB#Nj}9`C~6|2^MwDw*RNfiJld+C#-&=T~Cs9sx4aou0AE-yY_uYu+<%hwq*rY%YeA6 za7t7q6$7b#y(MOOVGzPGz{7&jOyJuE>TkWUCd|v=LNdqoL@WP^nSQgtDy@yq3OPtMuD7 zV_KY@#`;;H$oT}TmQ9W8x6533RQHNDcow0EXN5LVH)uY8c95xLpFU=e6s$;A!{(L$ z8vgjIxRL}$85eC!gFAo@kyHkQdjWy53@QKBm)Fs(J|;V|{XG(58Y^MMy$C)1-SP{_063}_TuL)Pw z7Aee(0se%}qfyQmka~F@2ng04T z30zYp#6E6qW4I0&EUTyxO9}XziG5Xd7V^7| z0UURW^76)O)NuIjv3Wzls_;*l87|y{ED9_*tqtdbzmm`HNJt-?PD6^Bv;qhgI_qqfBx>k zQ&Q1(GkCVoWAeyUOj$~e|xjW?7es@6kH~@^#y~2zjU0jTa>AYPw zhfEv|Up{kd>IS)NPtQ;UwT7J2ouSM3dfM!Aq&`&RW`&IWCZ@)FNJ8b@Vla=9CZgwh ziR8Z^Pa9U=)C8#vsLJR`!a&2Xzzpc{-lsl4d`dAJRM=??PH&D ziPJ8;52?HoZ2Lsr>(db+WLn%j;qQr;our5*bE3-DvgNtZ)UA9z%I3j5YE63;q&GPO z;l^!XD3|dw4(pQc?)+V11h;8(Y3P8RFs&CG4PR^kcc<07^n(XvuLSLXUok6ap7%mD zO`LNjUa=YfT)KPn$Cbh@>cN`wcSbQ^1ksR>*y-h!{?~h3Kf$+5Eju_50np8#Zyw`U z9Yh^diLS3VVBQ@M&72k!Glel=bUAnioX!mW*Z(aiL_-zPMITb=ffRL8NqSWqX@#O8 z#?E_z_y|0SC+hsJC?s#z)lQw(j;kx3jeH{XFffu_BGu+Z$B%u7yuyG$)1aaMY16BC zki34hf!;8G8EI1Xm%dO>@>{I_bZA=f-$t8A|7Mey#m5&=JM`2vd8rNXeeU0a`Mg8% z7Y3H#_FbGAFCzcr7kDcp!iBn%VZc1Bm?NM538_QlQ+-kkn0XOPebj)7)#-P31`DBO zT33rDG3LC`N6$rL7JfNb4DGkmUmMI{`JQ`*1U1T#gl>L+J-?)`VO|7knhUeRBa^Rl zlQcl3rUf=hLAD+T%M=5gPRri82uzZUO{Hit{PYBoOdRiCCx+spB*$*DNK_H(4-B!A zs52Y|a#T><`sog5dEUsw@ox^a@fZP9dI`af~S|LqUm z{TA&vekki8*mx)uo&D+0Ej2*zyeH%T?EhW28(t59{EDf1AJ>asV;bF|QalJ}__taK zo(K{Kj501BwRdn3A)vCajVCf{*GJyyIBdnH+J!QvaVw+^xR0P>^dFryyliig(vzu^ zr;SoN@L4@&>wkHBcF?vquy{FG_x$K)^d(~s_fMYv)|I65?2EZdKQAw{ikblzA-_!# z(&qCK&$csS(gTAA-?i!DfHktVo88}3X1t4+o7amsSCcYly+#*(DKdJY#GhSQV{}65 zT5cH*eD1GXkER2z8Wz2FL+|&P{JbpB^`2k*9DG>a3pm4BY>0JXteazVCdpuvHvE8Xl;OtS`De3*f;&sx7=NVaK=h)xX@&o^qXEDAT zmG}Gsr#FxI&8XYfnieiwLl5qy0*=k<+O~jefa0HM(9*_Te(i z%#ys`@V{B~+ABLKzAS9JsJSovJd$AKb2AxmJGtFhuBV}`OCW0=sI+VKGgsS2l`yBl z0W?CP1AL<8Tjzl_AMPHbyl3s}!g|a_dBXowTgo})8)o0NMBM!^xhaelfbarK3qDK&HH|MKe%lCRR^$e$S`QyUW@cv}u>iR? z3r4~am*vk?4}ZBd=zjK20yw*FK~|QQ-Dr7UMe_y&O2w*@9UI*)YWO_ABojS7Ap3q& zrHB)y^ZTcqI$OU!agOHnoIdq1D6D~64{+lC0t8}YS!E~)YP*CRYiVgIFik=ruy(-4 ziNSMk_jiW@{L?I1Hxi=(acS4qPM_w5!QI6UAW8wLo|fcnu5t909hFpyn%mXxEV%59 z`ih+xw-O>M_xS=6HJ8HlJWga@EU*bE3Dl&PxRZPb`q2Gre{%8}5;5i7Gi5*mtCY!A zn;eoxM1(tn1aMepFw#qmB5Ya~=2_jlRD$VS2xa-FRFd6{AgWYS z0cV1asQvPn-y~8X8@MIz?=z-t#xFGKY4CvY)o>k{E;&2c-AkmGS)h^%NuaLyRh}7> z)$U7SEx{|jlN2@})o;G)!aLF%WyIeI9RDJ1D@CFHqclN;P)|@8*UqbaJ(^sULC39*)6Qxn}_l@-Z-~Q+=dfw!Wnx`=OltL7xBAVts zKN#@}u-bvwycI1g)HA12l*rPo`3^^_Ok7?GkZiM;OWAXxY~aaA14^)WZ)GAb`274l z=-y7b^@gk?mQrE#cT*}eLN zgpKf0DZ;8jQUuh1>FRQO&P$wBeWZB^0WXB*Ppp)4H}?J4Af$>aM!JY;1ArNl$l&v{ zUqV4Bm(H<=Y%cs9n>>}jT6?sT3vI87TAr0GOVswO{b5s^HwQpH$W+DeFQ@nv(YwZ$ zRA8!8?qDYu+l;h`Jlw!bUp@%N=ybRrEvuq#J)W9o-4`2%6u0H?Uy-fjO2xts^DbKY9!iQd;GEJkdX)EChUrpS>At_sgC+CF&dAz>y?ncw|HUJM^{spr7*- zcIH`N{uHTR+Le+?EP)`kAEu&N53!Ho zLzn3^p~eVB8o*eZ8(gMMgOfVMqe1a8Q@n~|c)#VE5t4dvfTLaW)}kNllS+7%TbUDc z`T&Cdmv5sm1dg<*Y7$M)iF=(YRIoL(bRDUmn8PBdqDyy(I@1YqGKWb`b z5q`gU4CCT`Z+nXqFE9rj%tR=S#nfE+*Ds)UW6YY4#2L^43)GgUMHP+a5f}!1YpT&1 zx1Fr`4ARUJ*Cy}K`-O@`V|-dMpyo(+=>7alUwK%z|CMjNFscZ?)Y9?{@>m1qeZ2*R zRNL4>Bb1CO3BW9B-HQ)^cm3FOM5SE1!0yw&ay2G|0!@)9gc-t2sc4*|4QHA8AWiuq z+2wWV&Wt)bFRiu#YnY8j*7fW}2m4o}J?Phsv2_UkH4l`H(7{kq5p9pB6&D=IRf~G8 zOc8ILfi#w6z*SDAk}ScDX!V-mWtp^;u|z{2&65f z*og_JuOU|zboR`7OJ{^XM)a9Fo%Y!e8pEw1su|Z& zlkKNEL{lC+ax6CgOug-DwN?=Cf765L*|wjE84E3DY$2=W-_f(B919bfL;o&L|M!YH znkjslc_s!}Otesg2tExPCo{L;grd5reR%@9=-vXjWQQmZT)Cs~fcz7ySE@!AGr!RZ zlg;Z=rl4gM{mX^3VID*8?SG5jyRIAj`^|e#7e&H(Ws!&73m1ET3cBx?`c;x>+s>~S z?(6O*jXrGTAGA7O>gXpQ+>%8@E@%@p}Qncx@2LjamLH+r+8#S z5UxOCuaD`cG(WT#xG@D^RO3^R;oT5AS|(V_?SzDne#(8gz|uc;XT--DVxs4tDh20! zPIX3RiojJ`AsKUZre=MmyVtiJ`u45kf?D+3GwN<|hr@x>b2+MV+QG2|kfD}DJ@$t$ zP&!!>Fmjw1%e-c_;v3?RTpg|<2Vn_bUa}&ti_icf+!&^0+zFyKbDHXj88A@o@xCT| z9>QAsNQbSi;E8EyIfX1ZYGvsS4$_!eMU?Z~3*ttp|J4F0RK8!Aq7V6d7_sAa$xN?w zDiA)QJSI?3daSAPk9m)p8ZiS4;wKN_2PGD z_=v)k$i@ha0@}52KuaUCg+Xy>bt$*q<2JGBBLdjZQjsOSog$U0`LfljQ2B?U=O#OyoheL|~d5(9U;^c?NI2?#9pDKTg(5VYzlF~(&Da11m;!GmI zNe}4m`F?Ks_CUDpr1V@gu+~c2E$(dXPzHYhT`Ukw+sFJ_O!u~uY1}E3QAv7D;-Zp> zoRU-ynOX=>%DCX%f&Up~rI!)WvYF^rn)87!<<^X}Yv-Q^Ex)o;`9;)p`rJ>J+l|}u zPvym*Qx?b{T6p{UF#Ip6nahO-1+JO!69NRDs+e+lzQ}cJJrRrIl^a2O?$6aj{^td` z`6T&xdFNfUR2`M9UPMaf7?r1*DEME`ryl!RYQWGQI>m0OjfpV8-jht%|BxBV3ibXfPbMsfdCyh&4U* z6t8?{B2e%*3OoW5I)wijhIuQXH%611&hvx zkZ=NsJ}=p?VQrhgMua&kKu#y3h+a!1>^necj#kI2DKeqi^Qk?o-)WKTl-v!={$+K7 z3D!Q*9>zK(`g=h>!kwTP%?T+ViFug!^WtjW{%U$k@nVanvOa;`21|OfI0gpI(A=#} zzk{o_kaw@U{#S|Ey=UY-8vRL#+|f;7QN!9F+imu8_}1&j`~O{6%08z?S4@_L9~1yw z#etHJ&ySGp>Wr9J44z(?5r9@6`fZmM=lCR21R5i9%?O0t8aq0iHOy^A?m80q4VqaT z{a1GG_W9Q-e>ThxwrnMx-~Qph{WG{fc;LUU7O+0z*tWBP|K&gbl{)`fiJ69rlTjn1 zhZJk8b(pb9s(0sh*99_daSex$cY_a}?@p3jeK zm~MMQ2M_qSgP;DOefZRhueO9XzxQnQmg!=F>2^3Y`@>b?v(+`LE3@aP3HKe!_p6vM zA9lR!a=ia}e>i!+Ts(N2Z*=Jxa9kB|M0NMK8;j>#IzeqE!xM<0y({5Z?|f<+&5Ivv zd7`+&Xblufb)X&8Ig?pXN-PKeax?QvVneKXnHb^Ww}A)ozrCqJez-ic06wfJRO*c% zd>GgvV%t#1#V?Zpg#V;Zqx7-ceP!?g1ijt&TQ7In&%+wMjkMDIE5x@e?6RR?z%%_$ z72^&1SX(uZTT#2@=bq(Y`iQ-yBYw{4tYy1Z!&k6G*=u-}!9>u%{iXxTxM{ltWIO>> zOv8+P8?FY!;I0v)7^+`~>1QM+sAeP2Hhcgr4NE{(5f|3E|H2TK+~0`*N&fvL3#gl! z8~3AKd=%?rK~^$B*SWJdeqYk5t z_{^FMI32CN0P6j3xwTUw?chj2@&Gr71d}HwNT`21_dmQkK{faP;|BDwKp|d#6nia`g;QdM9XKW|VZE!ZoAO?$##%&YJb^>A4C4+AZSq~Cg)|{EuFWME1ZuY2GF?4H zUly}c>BFfaKC56Cv%9g=u7G4Wc}`bX7>@W2MfL?`q3KLrr{cB)&Z z3(a)986&W=HEQ`v^}uV$XZFYK{Nn9brU8V6_9#_~$IBvH(hU($iAO6oktVIoscnMw9l&V>{|qdeXcCLS4}i=}H)z5pD-VG7G^nNSDSEF?s;?BK z$NxURT?+piZ8yW52f=|-kn3t-NjXx;6U~^^Gko2fWCwz<%mqZW1#3j>U~CPFFdNqZ zWfQ;DYQC}{VC=FAC(uZ_mc;s0Fc-0ZC}8jx%jI;e8UZV&Y5>FdO{iAx2Ns&}W*SJ<28DXiU<2+XFZA4l}pg{)V@qWRq*}6 zCD0uiAAix88wA-~*Jj9&rTJ9V-*xfXUccI0Kbd{U&wX*Diph3BqHB}MwcPzIDa+xA z<8UQ%M#SB1U&g0WWFs|3r0b%!(&Ky{r85pd@-pGUwQ6kI4C$@}&JBjj%Zc$M7VP`2 zb#+vz5MDvoK8atI^t%x~N1v%u8?uEF6T(rC`HWi0<(=~K&h@XpqyJKv3jX%I^@Um~MpV_-x zYGHYwiqF99JnxZ6IRyGLBOE!n;6&TLc-MjHKb^PT-|pXGIKuWr#h4{lh;Z6ibaPYF z06H`@l+OU3tHA>%S;wgOryDES&$6=0F-<6+j2;~4NdL>9$3H0~x~YdUCEtDi)>PY% zmKqKFPqbI46$?j;Wy|0!q0?I83!Tm@MnONlt8gPgmd8>D&7<>ecNCRQhqvd?wbVFE zcjmKR1%d@JA_0+p8ngvImnqkvaZ%!T6pBZ$0uas892gzg7<@c}nJxj(Tk0$k{7pL~ z|4APAS@V<`z1V^emq`Osk(e9idwH}rZZq;Etm-pn;Yyw$VT)civSZ%!Z)RHM`=%~* zQt(`1W0)7Gv}tbUu^VRK{SQx|!rI1@10U?p_3#{@o6lW+J@DOY4o_0tN4(57^1_ER zk!j?LKf@I_o%G$>`tCFZyZZGi3A*D)4frMjnmmbm!dKysb~0mM+t!fF3&6iYWmjRX zbxrm?0`4WRy46P;bvROO`)qo72RKGu1kBEw`(TNUB<-c;&mfJH`Vy)ZbK zanNvjL-qW4?svbv9m(Y0-Q68k+aa>@6&r9foRaN*llG`C^ZyWa7G6z;|N9>yB?6;C z5Evbj(hVb|1qO(e#E@=Ahtxn65Cledj1rOV4hN`ogLF-h?%zJ&`kizC0Xz53^E~gk zuGb|i`f&vLpO<^a($kJJ>5fc;IM?q!*LEvMduds+cWW099Zif3j2_Qt<~2L-oF3w6 zZjzcruNG~_@AgAao;B~5G`1gB5l&Pciu}FpaO*tUQ0}~{4zb@Ze65giBg0E*dcGeRp*79vC1 zjw#P+prhcx-w_JYUmgnn2OHKA8oo?8^tF7nCIQi(o_kKPvm>&;1>kScw8Ot!84&S2 zN>RiUrnYHvf*Ln6th!1jewJSCQ3bzV_{IW7JTMdsA`7I)OE6?_GT z&#L*#Uzo+UDMD3#nC4B+EPnzpGUYv6d>nuAYaFN>hJ`pSP}xy&6$_AvCekaQnW0R3 zcL1m5dN6ct=2Ocgi2hYPC=>24RwqovK5eGz8~-_Fe7D6_F1OL;%{PJd1KO-AH9#N6 zkkuLlhx4WVP+xpb1Lk*yP^KHR81W4i>#q!7=NvzIK^mrLx0mi>8GuDQU1RNV5pM2y zFfdAPx)274fBC3c$9zkQvR~DAgI4@}YW)nv8ytG))+4dFKA2WJ&#-^|9x<1XHvQem z{)R~jks#%JKufVgY|}pEC6>MSaPmH3hQIR}b3p{?)06pKIk;?m|4?SN&t25cPb5nP zjqMi-;Ja1UAO3Z{{$ycvwT$cbAG`00sz~1Ju4m0x<}9}r%J~C)V4v56B1rRELcS5t z$X^fdh*`=?*>E1^HPm0_jNaQr_pnk_vI#8j8axQ9>p)4KcZ(|wYV<(o!4}S$tMkiR znINUzsnIk1-j~o@%Nyit5!cYorx45@q+rqErDuX0n!;4!pN%|st{DUp@2nN|N7!L| z#{&v7{HlRx*T_s6g^T{mg*s3o`MW5>*UI53>hcy{Vy2Mgdcd(EB=~K*2NK_`c}L=^ zDhPZvy53-+!wM?OfCmYer~N2I5^>i~TXf^4G?)sp9gA(q9r ze^D~yW{=f-WWG$0s4Lrw)LCfa=<4@gY7`(XBB< z#kg<1t(a1qj>Eo7=A%aNvf(mDZKg~gZ={d@KwKAh9=qoQ=uwrc)g!8w*!yy}=)oml zuL*8T#;eZvp(1DW`$71u)oZ@*P*gZqw4M;Y zpJ0R|knX*bch{(1B1m$Y!bd+;Vz-vN$Vx<$#LlFKM5IENUIN1z#y*5>4lSa=hJYaH&A@IC;+aus*A1)^Y9)eS7TTArPTDfC-->l&%{qHfwp8?n< z`U8Idr2MnnbSyY)9*$@L0(lewD?C~acdSQx0K#-^Nh^za*LGo%=*$Jpu_YxiZ1Gqh z*c^2Atu^$1Ek^6@93x>$;wW&nG9`PK>lp*TEh%XT`u<_BzNYie?jqzI7})!C#e1`7 zQ$YTdtiCSh+4ZT&4SvmC>fhUXdH>UDY0L9(Pq#YH(4^I#QlCw2%xV_uv?)-qMh*eV zo8NbPsk}G3OZ7CU`R9%Ja_75)Ii3Gf|6VS`9^^YaP4_i8+-@5F&Ud<>O)y;xzR=qK zxI1-sca^NmB$-Q9>>$Y5^3aVRzv6rSrDj4tz;pXlyQaneB3w6gW29Tdp_DMbAro>w z8*w3Ue$jdF))p1=upVjZ@L53zAEg#=hOqHD{Sw0@F|_n5%Zmy+$zUP3M?DR$<{3MJ z=-AG~x3d)4huLsbXg_rm_MyaNq-q21i>l-LI#O(Ls6soD%_>9RIdbjEuxqyg&%G8{B+sQ4b+%QR)oNO z8yMQm_@6Xb)Idb(fUl+|!>APo0+yNs)5rmYN~rv5RJ=s`CO^9#3(xSv!UEEy+DL9^ zF%FmWX-|i930S{Kf}xMR`%&kgBrDg7v-ivp2*s};`vDZbni{mhc?Q|i7&x-@yaJ9v z-5}+Tx^VW-0s;f&5L%dOj}=&d=ve=)u&}VDdck+|=i@Yih=a-92|lhM@%=?1uOv3C z@M8NkLg7ae+elox*s^M;Fic?35ysr5KEL@s!eqqCv%a#!jQ=g(TbMu=Ev6BSRGQGS z_|u?;X^P9(KRJWR$RcI|;^R^0eS8Q+GsofLk|K0#3&mnD%8YTV78{v&qYZ5lnm>zd z`K%AVcpBFKULsdW;VPb-^hAq|IyI zz;Z>s0Q?&f@4oUT{lN5oW6k@fUJKddi)Tkif=+>(BkX|fad|cHOtm?V9^^^bu6Kx^ zpWnq23}$}urt>UxGb{9%CNv50@OS0P9H#4n(N5iNSPHKHQe(22w!AET;1%gSJaiKk z(Uyb@`iquZGMS_2bD0^-O&gh%MTWcU3+^_ZuGb{!!iE_m`p zJj>(aEMOGnPV?tIucs%#^~J#VSMHo7m{f0(v)LpT&^T&)EW1vTwwZV|U?~ocS$t)% z91~iXvfzdWx9iKF#g>&uZeRn}6q$_rhp$Ah5}fF2;t04dX4N`Y0iK=x?Gl~83n6)6D~>5|_pfXP zS@saf;o)Jdxc8bq=0kG(XO0aTsxHUq)({G)!8LZ+j&Q zB^6ih)Zp9h`*4DUrWN~^qULD9lYJ%bw`r#hB*OFT409-Z)FP^6y?=bm3o!p-wxX32 z;;9OJ;fa?g0j&r5gayncz2)V8iXWan`-@YoC`;p7abd{lGj<~Fm$;Uf36|V*L~C;I z#_aUF9*f$c=gD>K3Wjh~YjXz|KT-_j+EfBwKn~Hk)MEaQ$zzm?Ap|n!}BCwChcT$qmIb8Ov5;=j{b!Ki)biC-e1f z%;zq(J9fYW8N5P4w~$DH1s}Dlgp{cS=H9q>uC{YwpSN>Du@qe~nzry;Qu!0oZuY6e z|HR>KtKJyb2%l(+FB7ID_Sf}P<%HgR1kK;f6 z4KAG8k5+pgr?q5xrmD1Jf@P=B&f~lV2CWOwr66Hmpnn}P95F2v*O#^!ae9t?^emyeApd@HO?%33^{z)1jbKf_C z3^?2iZ!J9|2Ux3s7e!VjMSvp?k!6E)U4|80<`o;YNFHd5-|>6+0UA9(`!L2K`?k*tTZArLQzM*#8Cv4m7n=l+)6j?qR{Icu`N_iyp4p-fK9Ri-jGhEj zD%oyHLBBqx3$vA~_A~bd=@UfQCv!*In8%Qj)DHbLk_Jq)WN2|r_$%YuKAKxz&LNNu zJ@!{ir2@M{FZc+lKlCm(pvDgT>lr%C_x#x@`&dBQ<{~H4K|0`*-4yP`T6@{{)nc*5RRxNNc?|hB(XFsXue(Sf8O?f)QY|i?m|6P)m1%m_HS9 z)v(}7k@Sr-UkKYbE^ z8vG>d6=zEDju?ITpWf`=b1OOv$Xi6c^^PNqneKhpBf+q@5Q&fnveYGF!jIqCWLw+o z;1a7t%<3GR`cOL$Wh!<8^ur|Q;b6xuRZZ|ju3YfLzQx_Y@t~ch{Y?!+5>o!)4Uyc? z(Wf0A+bFkoG{iEYFhU>*ZD1G$;UHam?iRE(em@&}HDKJ1EhOjf(p>2_o9TBk#554L z_pTOo>-KPIApg%YXhjyb;`Pw0J1LG@IodLQxMI5fFm?YvSiBYgTknswY(9*mFTFI%mvq(6%3TEUaXqPHUX@Pfr(9K~ z;u{Opj8+l}?(+%tWlv_~%vV3o=b4&zX+_$5LC69^D)reUsmWbGeJYrwyH-C^dg{n7 z16NAe$<6=sJH1(^Jqg;PJ~FYAfbIq_NjzGr8+*u42vH)Er`+__OpvPN z%oFm8hm%4G@=gp^s$g@Li&RnoqX4geeV|T~(QB~RMms(|>#?!=mF<`Jnu}#s{|OIy znZWXfXB6t-nZMpVZ3XzI;MFHEU6^!i3XwNn9Ey9O!0l9jKyp9>^5J&Zhuj;Ed*XOGib0oz$-Jd@-jH5BAXS+oFkr%;=ACm5^ar8{!B0i zk;AB>S%riGJxNJy-fxkxg-30q(-SCnmm~m4gsnMYb;M<_vHiyg1zG;qr#)`&ctfZ` zjpl&%aW!kM=h48dG4`?&Q3EcjLa-V;Trzd}Ci-n*WZPHXkI&@plh1j8o7A@O?*spg z#dhpdn)Qexn|1Zi6$(7Q#tHP(dg{1tR6`N76?$o*^eO_XgBVZ*RgFUiW-=2+#*zGT zSol-26PxN|77VA=>=`iWHTrkPkn-wewY_%o3rtU{EI0~TGRY*r#@@RxDT0edAQZQj zKZMixS;h{|g%$9m8C#zY2AdFt|A_x*lTPWTV2}4gc{_p{o79~GM(SfAm z9c`xB`SW9KAiZ#>LVk4tR<0rS?|;vu@~f-&S&(xN_boNFA$sGsvRLN8D5LH7r=GA> zU$G8YbFG-49GREF*2JPdw6oJy?GK*Y$RzS1MJlLF(x#ZGbqde!U0H z(^A66e(6q**+}R+c~!-9$?f{rNF>rWX7aPLC}$l5cC$Uuw+zujM(jhpf#9B-fXz_l-k5RpdA`1t&M#!Qq0Fx{^%d7dm>o?&NhEW}6iLBk~Sxj%7Ad>k9z?9z+vF&RiYabVe$ z4<*Z4Brzx^pD3TN0V%sv$6$3Yl`b5>Aq0Rn|8R)QKzx@Efr|KSd?w#(A@&7%FKK{ECkJSkoDJp( zptYZf^K%wOKM-Uo6Tg7xrC@)kO%sc83(~#n!f*;9Haau-dAfA1#{j;F5}m6J561Op z5Y_bp)9Ij7X}_A#b>)^1%`|G?o^Ypl)Y2QLkFulxZ@^ab5orBn-)gr+BlCMkdB?Pw+cly(k_f#=MSyv)q)f zOy5w|Idz#H@$L%<-7l-EAmMDC*s^j**k0-!kpXQNwlxQbb-7Yj<8{co=(EmO!>%RS`A98`=mybPM ztKCnFhwg889%;H=4oEkYEaBg81kWp0X;kSJS&zmyWO!2BHi2FDc3#t{?eBWK>Qq3C zUlM#ZiJJ7u&){p8!qv;ye(XmuM)9M*m`}a?BNpx6PoMpg9Qp!;PLnXU?vOQV;1$m? zdCR}Us$rh`;HpGRn5rha?C`j+0nRy7lV+{o2hE8_L?DkT`z0K(CqXJOlNY>s4(M$b`5^Ao<5(i zpCsP(Xaw&d_(}k%9N?Wy;RWDJ6z+{*tZi@C1Q%ko9RzqMIJ9{2t9DqwE!oX<)POA-z|**%)OXpHo_7twmM(TRdo=iQ53h<5J0s zRkKer?B_%9@Wv8ts?4_5C*+V~z0vIny{Y8kK2(nX79y)I<=V_1#52r8h?|(CR|D^Y zFviMEhNH|W6i_P5-H16}5176UIae9Yq@AsJ9cD|`a)ZQJ0w2@x7wcSHkYc60vf+;i zF(>mU_LKC!)xPS^!=;a4M$2XD#J-bqN%t4a6!MzFwNgK;cZ42FwFYJiC)MY`hhBkl z2)m>>p`V04k*}~z2xpHgvmaWU%P`SW#JOMe_Vyx?m<6jGAVKchWb7&?WG6;tLoM`F z&1($Se(((TF!8X7@x-jOU$mpIryD!!kv-GSIbKsQMm_z#&!eAGBJryKWW$zsBQc`+ z77UVrgwhwLi+D`{SyRIOSxwQ#IsK+lQ)FSKa}O>3W&t`al2Cg=AU47BB1j9e@$?td zmX#QrV;z=A6l~0X&_E(GqO~n)$5$HI^{pOcD}b6)+n)J|QKx)8533jll!0(*m%D zTRiBi`i@Q1)0ZjVKwloQ6tjT>10e)cRgKGgXyY0SpNk6PbI951sshBOGT|n51Pu@W z?L^!}n*In=yPW*Q8DtYMDDXCwF#XV?^>!PQ3AhhQSzhv$$bl{gLr4W$1D2x8tF z8ZG@J7J9F_sh_crZp!3?|I`Li1aDXb|t>N3)UYEB`&va+8InB1lQ9 zg-$jfZ%a>UVqhC{7*S_RbLc7#69Ggf^u7%jvj)?aJv`J>PxRW>bCVXy7JE{+;;~Sb z;j#aGTmQGil6SW6zSy?2WL;tPz9W5!whOdkN@H~TgIFXPSVjW^f#UhFKUtHgfbC6k zbIQTcS?X;^J?y7ZB(r_JFB8PPM4n@B)6Qzgz6-SH1gDb0cg`2wjHiq{ucx5r_MzwY zKIWKT6DotyYVmn&1%u5jjc2z?ZUUy` zs}6w-7(sVf2$-9w&UyK15P6+_z6Aiw6#C=FDVhVRutm~L!<4Fw480<0ae*FczCPk) zg0@Uv!uG~KAY)F@-uyK2@Zyt4g>5?JX}{tW!92!i;J#XF%pQ0NnxE;`7LI#py_*Ar z!59OMf7cD<3@?3ztd*E+s@6ftorDO5RFep-jKsm@hRmi<=d?xaw4>SVQj-x``OFsLUjpWp}Rk~y`W-yM(d?UR86+?_cAOdDQHimj0 zey@;c0o6ku_E)CTWB)U$&7QSPf>2Fs=C%Kzo+`F)`Jju=4c$V}66mdm5mMn`&pk?ogGSS#UB16k`kQ>`gJw-=Z$-zhPulPL?PnpEYbmij z;S-iY_qDwHx8f_O1A&&l=o8Z5jeqy#$yFtY`_h%OJNIi&`J04i{zqO?&4k))1sHJd zT(`kQNy|5h`Hbbup|sq)BhF|3Cyk*OH4kHMAs6DE=YQ67uX?tbOxbP2`=mDa4i3e~ z?I#K=jJ>K2QH6(SmqYZ}MQ&(I?dIClW3|^hxvh=mJ)Ry49!_=*xvhb20dwfF^DFxt zUl_Z%FzeUHZz7X~S<5MgL}9$35)&+wdi9cFZ>cH+jS;DY9T{YjYI4BV!hzHxU2xGQMq#(8*_3tl(Q-eZk=4&_EVW(#IH7# zQVbKx?w*RB4eeaYZ^%Th*;d|)@*;n9X9tC)$ES(wO@~>n;kfSk>dG%%PyOkL z=pQ!~sk5b&QMdJpX2G2k>G`8j?Dr=8nCMWG*wz6qm9V~HqJTnc?#*J>m%t>7GexE? zg#$ris`@Cc74VRGA?{!Q$P|X3xKoqWnMjh_!S49^23qWI>;yyM>D;*3v8^qv#T(Vu zbcXM9TM%P)l~zAWk~9)ur2~~jKdF!s6TL8UOdHX2?qhvd2%T<+XPW75)B`)Ym@1?| zxz~9?$BCm+aQOQn$B1c%onbj%J=M;A;aYs25Owl|Up&Q?J}2XuB@PSJ;e$W5%ynC; zd96}BJJ}N%ZM^^5tN@fYQs&9h>CAtdh|UF9+SVwW2_}?(C_zv+;;t@B61C+O1qD@3 zmgMK3SxY6BU zU4e6upTv`)oIm8q*k&;U&Zva9cS1;=bb<+uURbJ%{0cdE$0r zJ)~wBLja@#vWnP@b>(RsT982}?Srd(UlLaRF{)dM&xX^5`CQAVJRe~R0#u8>f^GgHle=e zogFbkq399QQlya9fD;1+G;`*>z?b2J&Gx}?t3{AO1n+s%(5fKwlO-LQ>opIzH0c5h z+=_>+KZiKykNzBeq_c%87~i*D4t&71DjPS(0@U`$wN`U2nppaF{p9DKM5Wso4yKN$ zDInpmJdLJ4(UKHC!@`02)N>ISrUr;;ZdR%qrXQ8LXbQ=d?Vx~}NM0pkw8cv@4;GD# z45US6s&Z5aiHci%uuC&J=wBSSleiO49clwjWYD#h?1!nByFeR5NN%#?Hl+1>&Z?nt zw-s)bftB?qG>Lc&w&~C`n_kT+KYxB@J0+`h{Nke*1w!6rv%Cvnp>rEHGRcu+4m-OO zY*;mtrx<*IOQ#E7R|ifEL~5ZeUc5mfd3JgmTpGn~tEcyUt`wmB{rLUw0r=oNF!(cB2P7 zQDi=`u$^SjFL(Kq#`0l4Ems!Od0~+I(gTmsd`5@! zjYIZ{XQyIjVWmE7CDYPl;kkG}B-?oZ;BBbK7vx;v%E{m5doTI3>A$yYjf;1`LvI!y zwszjxd5n>p4vyHL7&Nw@cNpIdUva*)?e`wKlJ4AQ+&O2unamwKyXt>+A0Z#OA%9ct z7C6@qll|9apC%(xjvPKR3Wl+}=Dee{e zSZ@!NAvMh39MYHGX)V~hlIZ29tb1qYTS!#QVzIJ=yqaz}o<%X^rXV-^%s>PvVnLu` z{eDYCuao39#+32}-rU+MC=w_PZ1&vL$VCn{S7Tefk@nKd^@tTBQNTKsvR2L1{JCaQ z4q1c`sf#i3-7K|;^9yW|dlrxrJpX;>o+as3*y@JSUoI+w_vSpAW;)#0lpv5+>C*J8 z$=#)tL#i6TC@fw13z@L9mLDY_`U|EGbU@@1WJ$>Ftod`t@)FnuhHH-=z$G7bFX-)M z7x|FFmEUBr098;^1G7F(00<+T;q8ViIe=vJb+%!01&zB- z#-s?q$6UmVovIZf1$=`>RhkQggWhSCUigA-VI5T)l21R75x#dGg)QD;Fl*>#Uq)cQ zZ5=Us41~7Obp&N~?Yt?Qu{h*u54fiLrJM)I3rh1E&2AblVjx#E|ynu#>nD9rqC=p_o}DOCk+chiBDD*7HqbBBV>vajj))NCJZ+-Vuzk* zUL4o|CQTEt6caISx{I&HtF>*R=Om1W>DlG&#thT^zm)> z$xXBgKT4}E6VWTK7S}c!%%267g%Xz;F@kiO zLH-tO7C|eO;*t%a?UQD>-%Fy;=tl+rA;(KgqK) zn+diilt{G{`+<>d!ADF^zpD(OWO5b3={@OpJYFuSn*c?1Hr{+y0-+P{=~LlH6ZW{0#U-2xUrr2l#+_Ni#AUI_Sd_-;)!tm@F zR-0*6*1KjI?-GYamL6^KCEC5cF=;m#2QWkI%>LN}aoEazHcaGXv@z=m%k3fTo)Of7 zSrhU?ihCea@kizhx|+DI~+s?rO64P&OJuNE*SzlWP5$Gw&< zX7MF&->G^~!Mx5k1t@nE3gR+l6<-P}Y|WAK;VGuHnRM=V!Byl?!UOTB#s4M@PyY{d zh$5vr+S z-Ujs09O6qI0LDtXU0v-2$rvp~(?^Z|8w=7g>kxmQ_9IIggxr_XU@oY-N`-q@OfM~n zl~?!jv2lH)KP{6Ul3bWa9i+aoAXg!&w!m?}K6LyO!0@t+%o>|btw9+Foq`d<-^W%KLhwJFWtzv@Us zl;qFChp&5Hn@*|izr@rh9KMDcR-E{Di77qQ@BD40{%f|18L>Wmb`fmn_O0lR@LvpA z+j)0fwZr<}&wcIB}ZfP$$%C%IP}<^t^y1-wgLbli+nWK7Y{d~m$^YR7YBz2IkNR9b@F%XD;MS~|8^6C4o)onB>WoA{)mU% zEg6$ccOH$0ZjQQzR^ZRc*4Nc>e~<#vrS0;HZs1Th-AvFF*gfmeNq# zH32w}ZMovw({^hVC`H;NA`Qc=`FJj#z5eXQuQEtJw2EiXjiMdYja3oq<0BQ~U`pf) zUL8g)*0)`2dUDWsV$mu%Xf`2(H~=8JeZ_n^Pc+UT-*W#*vR8an2i^GN|S^a_Tjt^p7P=q zB+i-9@C6>Ke&#I{ps1lB^X2nlR^4M*3AaP^2XzNVWX=-Aq%g4#*qykvl!z7f#CcPE zhx;LW=(_0z&d!>E44eo~A<~NBAz@-#LbzdN6n8Eh=$&=Qj{|S?>9i-ORP$Gu0f)Ink#;YBaHgqS#(M}O z${Y!>CeyAecjPrV<0m z2;KM@1sr4K=@Pkm{|SfKJT$y*nE>b?D?VyxyIo~1>K*hr-G74NV>SH*;XamR5~apm#@I|d9?U8UC^JN6{)Sy&#svNb(n3l1H8xsxjSPMyb=@%1JJi&Z~oTE znL_Eiekgm%CH3)ZIBlHTHq(vo`WrahlV?MB_Pa2yP3xTb`r$<4Z?6me5?K-ifl#kZ z1$(08_cnN(GoFYDJ`oPN9qai_pi({)!@nkX*gwxZ!&GU|TM}@=9w2R)U%%nw^4s`p zO25|<*Gu7(FR>O599#J7&Za6Q1bk;|zS$Ud`yI;=yUtOF54Jc5Ma4~iRNWZe*#jG9 zA<7c-zwj4$2Gs6v2Bp999rv)q0i?G|z3Ba%+4?lF#z@JQi<#VOW{f<^fcaFP+K4QY zjYQlt@eNlCeFo!RJ7BcEmE{7JU5u(LUWeKKWQQH?23qBUEV5F_p-%zda^# zHq8I|WA$MdN}H`eU09eqfzJ{ET2G?CYUDe<*cTu9?Zh`oD=NZ^FZOgy=Beh$*M0v` zp7qafi!BbS_(+swDl3c>lEM5+EM%+5m!gv2S4ZQVzov6=Cfz}q8)ZpiB19h2kl zFPT5JeHs#8f3h%V__Mr%I*BB@N}y8HE-gfkzru(6Unf!G*q`32W_oN^0?J z8%bu6Srn@$8+5&P$H^qywqBATcU0W4tS^Xqy65`#=Fi|mU&z5t=Pl~-IMa8zrA$$i zlH2Z!+p(wUmNWd&KlULphT+s8(L3<{LO~yCnU;W4frpFG>9NO(x&{rj0H>`)Za} z`g6(=0(o1zC-nx$$nogoH`ZLKdV46#v?OeXq`GX2XF#7@H?hS5z`$@&SoRJ;>~lg} z*edm28Z#q^LkfA6hlzLTJ1%Li(%5d4EYvkus45Z;x6rgd`kf*)$p%udxH|gt*ij}k zhl8Xh=t{ilW;a`8?|#a9oQWksJieSf=NG|1y?KND{czU~uOcf<4Ej+R zRgeU#aa&qwp!7$wKYYkR7@@^+Mxh(>sH@p)+jA@i|AT@$l!P`*1v?z)!&k-Ev0B9Wop^c1fHHHB4bkMds`&s8Yxf&UxvJPl1WAoO= z)>hLhBBwY(k22lAXrB`8vXsToFB4s->~V|vR7$en4?ZP$QNZrtZ?y|!9`*297>tf zfZ4SYa!@6%eys%z9OCZ2Q4JE>Mz5~IhZ3kikgv;`@~M6Xf#{Q&z$r7&XRFQHa3ypO zX;1%5@q#3z%QH)sCF?7YquwJZz#uItsS*3N?Yn3BlXY2S<&=oBJKoxzn7QR%yfI`L*zjDeYvh4quE%`Z?#sVYb6ULz;j& zd%@J$r&i-y-%=BNK(&!EWHt^JWGEA4v*QVnqzVnZUQ%aqd28DE{7HuL1hfEIdZAN(Vah#j*@W8_aQiw>LC>d< zwxW8UX!|8WKHD?+0A-Jmkm=eW{!g{YY&oIqcFD=4#=SP@?4;}6)ZMMP^ z2Wta27ndoYyVD8X%(>mCZtC1FZsW>iAWp@%D40nl4}=voj?4&rgsn_>_qpEFn7{kr z8$V%YW~RtY3G?}`_YYJ{H5u>o^q%GAX8}gJ-c4pLfk|Q2`}5gIT!yxATX<`J-r73`p-K|W3Ed=6% zbeQp>5wI{Eb(tx5eCas0r~2G&yph-ukH)g}6Q0snIq5P=B+{kTVTvzdI(&0)+9ihg zf3*OccjUWJU){n(?dJEl>L+I4`?lXXgsTsY@o*O0c zfWE%U9kc`aPRNy-1ck#jiken!G$iCo8Qy}iNh}h;E zgF6&NPWR83{lJVr4{W~ABTU9Ldh*8Pir$5;ER1BBi=8^G1_rk`uP=f;~p{k1l*nx3g?0urPviq)vq?grz=l zyo4pCP2yzl-CJn+o5%T193a2*k;iuO*k4VnG`wf5M z;g*mbcPJO2NZCj5PmO?#O(UIv8fzfch5M7DggS_>WZhj~e49E`wtcMww(W&<{M$Y~ zk^q5Rh;erj@O=Qg)6m!abw`&=wVqqFI##(|I^}Y{58bc1&4eEfJavmC>o_gOh`2g0 z`vpLM9JezN**T2HNye7JK9!po{i=Xz#ybFg3qc+xW|fcKveTn#n#jkRiCkEOwD+s& zVJeF_tqQn)OE~T$$l3?_H?wQj4HHq71ka#^`TE9_F;5mi#Xix6%isRdq&n!|wk&fD zy=)iv8spHbv!w+Jc^83L$XE_5VK*Ts|6(lXoy7#&j<7*;en)x8pgX4;k(2+7nQWZ( z%S3T%H?A=;f7SafzKlvuUN;S8$oB~*)X(jo#wY@;)$^ZduUydf3dWb2jObPPX!C;f zo&aZ-M#y|?{^9_%M#+&SgI8n|!{yp*gu3vt9b)ENETx<^fF~vFx)>KL9%F1y4Vk+{qF=EUb$(;9op-v;tZD=2G zidn?YAO$*)thrnE&NE6JGB^hOmfgTb`%;3mPQWPn%ZM2)1w}(l9PTkD2_Yt@T>u1^ z`LsS`@qj1taR2uqL}Pd9VuiI}Z_hR?T9p#HgGN^)&lg1W=CX3iQ}|0>?VSj;^)bl* zOG;q=^Y|k7V)PfvvFi0k|Mp*;V_4_O^H;L6GW3pi5!O+FCkjAt5n=hX=@Y_OLM$Kz zMY;XyO24~D^IzD>*kx}p&CQzJKZsfXe;;{=#mV1t{wD_&4}&3xt-@eEeeh!vb_!f& zGQAuzc29*fD#X78AqKro-EL6Bj>`D^35qOAVD_MvyXdvC2bKJ=2Vsx&YeKDJJVmlk zP@`__jcU`K&C`m9z<}fXvG!cq!C_Y(KbIw7A49}?IfPMC)3qmFVr`YITMr1(!a{`s zw9Fj}ts3X0B_*x5jt_P_AO7AisGpw>C(*N-#(SWGdv7D$LheLzuTaoB?qW(6;I6fl zwBLpNo$mem!*%Ck;PHJ7ms`hG``;XQ!wO4DE@P%Jqb2gPl9n|!9~UYmIlo>kU*C70 z3WRQ**TF2>LPHq_#XE1ZlarGv3q3W5Ob0v9GIMX1R<514@7}T0rmWoeJluxfXRk;} zhk(e;ZrFhxH`y}}{g!vzjlCt&VkWV)_l=!5{h^n|@(=$snf9^e{BGW`la}x5G%l2w zvx~9O57w@cUOq|ZNhK07nof!al@P(Z%Ygs|c)(ls9#oeqfvp~+LBatWiw`57HBS1^ zJ8&NZ1PA~R6I0Xxy4VaS6nFt#iJYTeWPJo7{cAX@PNXrHl01UjEwjXVu@~BHm5%_R zRiY)DpN2S`#6nng+?1k7OhPJGoO4NkW%WE;UOlw_;H2u1e!@(JVW7$ghqF?hn1>+Q z$K4!I_sIL#w754r%-%4p*9}28XXcbi%tlimXORJ`tjH+94m0v#I>-`vrN9PsXf>)z z1zF71K!8a$BmTIc?=Q`U1h@-?j-5YN0QQu0X!0D=wDE;R!2mcw7a9$z;pTQfTB7gIy*a6s1bbjkoLR%oLnA%F@1Ocw-^Wk5tka>RmJ^&rYX z8zr!%IGfjxGs2d z@^_Z^j~<3oR>M$xpAX1@TjT>Wgusc7Pd4u`ENMU(pKxrvkVpJyJN@a5U?B8Z+~oI; zSvG8%U^{D}Vbs5@zQLTQIC3@aBHB z7id-T4+HGt;{&}e(}aGHUn}z9Y#_K8Buj~c_CG`|N5X|`_uU=tSqczwL@XKQ21g&G zu(_1z0s(@RlzhFkh!i4`$4CTu^A#B@LUXw|CF@K0mJUE$gBKEI3Mv^HYt+o^?%S1< zi(v&)QRQCQJeU5oE@n`H=V+9zg+bgh<8lc8z^0U0OB4Prvo$e)%e-y-(hgoW21h`# zB&E9_9N$vRKk}gX^|PxUg8>E$-r-_$_)x_0WKZHzcP$+6fAl52Z+^Tp1BWlZ#sV5X zQZRf1FnkOUOh#a1g0Wbz)+EjWNYk5+grulQI1;LWO9sX~0S3<_)4fQ-Y!$-#QH~J2 z?4spK;%wj}+#$t^fpjn~N{tnuP&Jk2JB(gbHg%lvMbazVMA~+zPwHtQB*cb?3DzZy zfYxZ;1oL69hK39Y`+XlW5BlO0bFw6G3Znkn!=PMr09qM^@S*~#DzITa7W(&7bE0A0 z3Z>ZJ;`?ZzuiS{3h4`+?)yRjI)q}f-SU-PyabRPbR_=a6$6RB^2aiW-b?KM~3?3RY z5f#yU8qMN4BzdC6^gZ_3T1xk#&(hV^6~?asCDFod($~;%;bu+$?!r?5rM5xY9GWU@ z=ue@L`5-RgbR%UPc8iRlyvPGrS-5_rzvJaE>kB1=J&LLw(@;mi*P>PSG>Z+aBncMN zM1&VNDytYIe7S_R*EZVxF%Fv$X4N6Oy&Vd36%YSSt;j-ucu?Q;?~NZ})oV*c=JQl+oDm~}`=#xlQ?xXh8TdNwso+;4WC z?%wZ=KHTuG^i0)hAGlUNjS5Sm`@Y|@w6s(jLuwg%=TpN-j49))G?GdjK;qcxCyDwu zJHNUMFIEJK!%Sy0PA>rdwcB(7qJCe3VifUh=ja77$_dACKO>xNyzKe)wmB6kA^Akh@kO=n=9#v8{+DLY!)uXG z6?Ovb&I4G(JhVyCDYQ)vAdwnh-_dv_VIZ4zNg-zp4+Ta6mIlUk%!NdsJOOOTFoP1K z$c#Q(cP+Jx`gH@lJXW+RfWWErU#KOcCYOojike8WtUE!(DR!(nn)9>(5yhhbw|(vb zGV0L|1ptK5Q3~TrovPsSD!|dyj4p6<_1FM|Qe9Cb*3FeW96V`l{43NyF<}E{`}TI0 zSoziSo11aeyl|7L51bhk77*eo^*J9hP}P))&af4Zu~ zVcGw$IVAYsX%`9!2Fxxj2-O{4tzHZAr(prsB?_FN{?4f50yrAGZE_o5LRN?`YR6nC=Zwx8=IQm{rf+ zt}afOll)B`!%?#Qod_g)JoNqqd^;`u_k~N)`CKdLA1ZqR+Hm#%X!^>4rrx;i(G!VH zKmm~$B_I-`MRIfxMuUJb8l*+KM@ZKIMUfmW(jgr(N=jNJrBfQ-{om(#;CvhB13TM& z-`DlKqQj((6t&s$g#^{lfc-@F1)cVbd1`1~9WlNA0o3{T&?xTEAaIQ@YX+B-I~~1w zegN}2{;=;Kg))D))`QC$n>k%Qr+vQj6$}d2w&eccSHELQV+(z=-Vftagv3~WWf7=> zOe$$1%yx?3<1+N)HHVxSl%PIR79XCxYfU_p&mJ;;Sg%0UbEjpE>X>Wt^YcSZ)s@qy zyyL@D<%)cU34EsMwjjdP9N0Q#tSTiXwR0u4aeWk937q9tlrEJ#G48=um^Hf~9iBp* zEdc$-(aJq@;?FPgJa*?}{}yuNvcOOPJJc2$O)CeSu6*Ug!iT(_I$O-284O2*g*VDD z;t8;-nwkXEw5urK4T~2{PYpJ4_EM9#_}-$q)7OW#>$LdTU8YwFDFfOv zh;m~>@`k@ndzvlsA+@B?W{<9cf#_6jySy2X{`Yeu`Kb~<1A@AfyJDzfYMzyU3ulZ( zF#%jiMGSS-6!+7jw-uNheq6w75`UI-IK>Jy|#F^BglVrkKIA7#UPoEh>elgkx4E2bU4qhQI zOuzSb(0cfe%9O#({y=V-Q#D^L)V(0}%NJJI&Xu!jNp?=PB64xg1iBBgO3U$JtSK?# z{leqfdj0c0>FM8&F~dt#c(A^Mz|e}}lXJkqQ7oTbNh6awPGmIU$->@i+zZ-7-)wg` ze7{pKU-`G=KXG*4@ftBS$I^SQ%XIY?I5g*a*RE?c8T0J3{VV@9gzhhw`i;5U$4eI# z__twq8%%ftJ<2TCJa=2RsLk4GbNTMMdR$%ZNIEx%vE=UEW#yeHU^D}ZP53CKKY6z! zeaCl=tkzExVEv<3>7Q@k^jOp-SjQf#W=_0t_c7C+6~CSssid8!5o8|7J%rXZ$D^%WDHK z2Je7iEzMy$;6^qsTDgItR7@6fJC$& zuLzG)V_ET1^seWJGvO?bifkIl-rgR>s=is*arer+d#afSAVUm&@$#jDwH6mvvZOtv zSbuWX>-~4!)7$rhY*0)oOb^*Q#Z}wW!@QC3VFZ%AIxH;g9!m(V914XCQhop~nJlz7 z_g$lbHv1m;8V=U@xahW5`mgc8-kkk=A_xWxJ?TrL&79F5lj+zP(*7E=zI)LNdisfb zstonbxIFWZOMM@MNAA$U$tpwAN2$OV)X1XWF!uI@@0M;!w%0gbQwzIu>gu0@lDZg| z#;F<64T$jvV_{1B)5dolOLxhb+vU0aZdxs^VR@igy@*FPKa~d!?$niPN^4&;vr|4L z(93b8^yW7teq?BxsHKM8?5tVnNXA1(DI z|Enj$^KoG)+9dR8*Fym|4uHpv5TRaig|t!wKcA(FJ`N54&lQwBMlb1qBBeqO6jCz{ z#bX|{$oy(!$inIRxK-^4-a9(Emi= zyvvV_{j8#XZ5y~(6nI!m8MGI4z0bt((f-#={7YfI@}06ust$f9Gq6H_L%Q;BLK&kU zdk(f(;fo=toIDIQ(5awgGdnooF%WkzY-Y=B~3;#y6rw$WBBi+q6z2z^hJ?-DzlY#e8jRDmi`z zx-VMU6NEHx=okP7wNpmd)}bg2qSHR<*+>%xk^fYW*@jtkn* zlhzgbS`eCR6Kkjn7kQ&zb)9po(jUiBm4nP~Bdc9fHYg)EG< zo{(9)0Kx)JX}mU}VZV>7O#^U*#sRgxEZDeM@rk|bbP-)K#GrOyFb5w~i}E9NTx$e& z9|{W9jR4_n-W|9M=U0`1ij~t^>J&Lbh3OrpRe8rn#DqTSA7|gj+nKH3}LgCe#T|ahX$#?Q|cbPpRRzxa2+NW9DdCW2`SD~EytIMtcuau7(1>x z`tof1(fZtpjs5SqqT4YO`6UiHAVZ-RCj>yKdf%0p(;zJRgSKjPH*)H%^-iQfNEI)* z@bCHn-=OusS_`Pj=FgdW`c8{cV*PE?>)=*o57oY^-BvABI{zo}Z#$>o>8=Wi&R%F9hwQv%rMPd#{W)FqQSBa z!b+bPjj+B?8xtq;!UF}&s>b8brU$24TFX$~Q!b7qWX|jLErS4MfQGTmiU>Akc=Xe< zSvE*_Ic7~q9tG1ww0CBzWj7__lf72oZCC&hugX-*D(VPP$a6W@ixS(TzKLr3M6Y%6Ejt!pA!BM zK3`HB$_}UWD^5*3aMh0>&I{EtS%%ya-dNLlhz z)61l6UA6E(@&r!+s?y!kvI6}yKN+j5+92Jb;z@P)Rz|bayG@1FxNuP#iWYcIiE#GE z!RYP@vZ6WKEZQ!z*{4{l!BK1?hj}e}h9>dse{pQ!{O4jNN~VeecMJadKVd3G`0a_- zfoo~u)6xYXFoNk=<&Y?;BRIOnhaGaesl~#7jG$b@-OI`4c?7t%U>3CGpXOB38$v#v zL>0?L7%@Nn^e`+;iPGQ`zx3nq#J3E6VMSVtIC_31MILve+;|9GYH<59Wn4hf^?S|l zjd-XEhYfxt-*STQZ552V*X_i0dO;O2M}0LU(o0K_CK=hjQf1~v4q2CE2GTeAiWwo} zU+a@u@XK}P-c7tZKZugP{^{W(b(L`!L9&0_v2=?QHNV9Q6i#^N5hb|?^%AOof;*H$ zXu!BAPfC1TxQC6?b zWpsn{3mF5m(vkT_UU=PHxgB@_)v!3HAb9joMjfd?!5usp=6sBr&x~m-5vw6sH(<#` zVY9TdM^*uQNfcthu!g1$*DV_5@pYIyf zEl8i^btvksx3?n9Y#Q%i(LsZ(M<17oJoqgY1OzrAAP(EO%;$hkgKcjHKVlwZMB4Me zM_mOm&cKew7fMQ;5UTR06wPW)LufOb!tU(vz*FHdIR zKbDu5#p367KVi7o`Op!dZ*p8c`6x^(7<|lsj~q`!oq~|<(?p#BA)FxCS*ku6ce+j7 zy&TTMlU1n24@OP6F_SWuV#+OGI(d~Hm$n{7eZt8Qp-oN4@N4Shl@q$#i71?JM zlo(lK>LH2rc?f!;A%Lg1!Fb=CdK-_k{3X9IA(1d4fM$QbxM94J#~eg>P;&?wcP*+oe+(2oY6zO+o>&| z2VchjcG;7%d|UTXB%6)O?Wb|hk3xrZH626}&U%Hmt*1Fz0JXEoD%Ynw9$RMN3I~y`eS3j3Jl8C$$LBK6;@D@Ojf&uavn%N;ctU(U z%sMD&DHBHpP*~K?+O3`YcYU^RN_$Y?qQM{@`Kvc^CKP^DH*W@l)SE9%6 zRUm^>9`36Kxc2ydZ7UCq;>3{?{{GJ(eBZD0&`%f`^ahr$ryjF}i!Q#!+1PG=r^11r z9vcz>yb!%Zk%8OWQ}~aInbETwZ(Z2xEL2nm>?ka(ttSj7609zh;PT~AgoO43M!yLN zFUyZe6_I+;g2z5(Eni%4k;ElUZ@UBqQGz`xAZweO%H`|T^vdHx0uqnke`I`V+1r=E z)JGdlr<~jDF|Do)+Yd$Jn+IpQn%drmxZ@BB+H7r&f&aEUztdzGfy za+cIw4SuUFnUU^YS;fe?{~rLw2a^wLCH&I}6DR8>!-Bd;LCg3_FVIu??Ida3m49-D zX1x)P?4%Z}&$FAGr@eiqudc2%sHV5Q#)V|^G4NL6b0s~e%Ktd@z6U_kpw!M~c?lDR6)VgzEU=% z;BODT>Zne<&2nSVbY0Dw|w`@Hwp zTd#uFmM+e`4|n#jhhN>jzxy}M#FW$huh*Nw<(u>3M3uwjme{|&?`EopBo+vEIhrb< zjXT@au2_r%(BmfJ7gnzu*UkMT^0R$|BW~f&hQlKPdhALrpt32_hgNY6C8TUqdkjw(PRVqG^=0<}%`6>UTqS*tRzb+% zy}-MBlp!sP)A=;Pj>#}WZCPQ<7>C->s+FRRyxk){fW@0nJfRYF3UDVKFxWXSArS-I z6nkh5hsw~Z%BFcw5>Y@?sKH~?m3$^yU8XPY2S06?ApigfeNtuRL&HCD!4T|LI#mPw z?@R0zx>aR`z+N~Cf$v84brD>L+V5ft`xzrc_E$;uS=?+Bv7lunaxgA@x#Lo)8J|m z6TAo+Y|im$o%8dTP%2qzr)xrnwSd1vGd%gnSdz&f3{TK=lpEn=I9ZhkE$-7Rtaahd z&^N&WjTMj>@BhnXt_g=`799I47wJcBQ6A=`7u1)9`r_f0sP*dQ_;L^CV&^~H7iljb z5*}I}-1k4cz3s&aa6m}{_$0Y?Lu6D2ywW-R%55s%sj}o57uaVNIJnlF8*(X}=A}o7 zm2_ROaEJ_B8?;G^yrmlEE%+6LkCRb__}(`u49``J8!Jl?5LEd72BN@r&Rp-j{iPd9 z-oy7zSL9~SX35mN5DBPLHUyjT%Y(tnqxZh7OWcG4sexmJ6MAGYa&CQ?IyH@6^&kHy znMuGQjO}O8ml=J9%$W~OeCq_v>MqaKg-SSC-*9!QUt|d;Cv{F-YD46H{(kEIQ0_r_ z2EN-_@I%(aiIok}Oan z1GdonQxmDZIwkmdB1192xjLc=Vu%Zur^Y8o#m3dY}nz*q=zQlXl$`x0hdgY#VMq6~0^Qde;(clWO}II7dh>yZ;Y zS#XKp4WdTWy=9;e1HK;UOm!$PgbVlm3TLiRsWq;j8@dNhbF(X-p)lbUCgd*A{FO!P zAk1o)O|2fSZ24I^)Gy8#6~|Rz>}6}K1XHE^FfK!gvSUVk9Wks=<}Ns3Z3~y*_ma=$rrZ1}W9N&k0m|4$t`QtYAeueP|axyd)qf$zt?WsiguZJheR7w@eG~Xm#uo zhn~?s=VXRB&qZ8aKrlfi3L1d z7pTh2ZN(-sB_PZqcpv!Kx>P@vHuLS6x+5eTt*=x1(IJaNKZ%1~z!V7v&s@K;ISwNS zK+I@l|8IcW$EfHV$6Tmrcb^*P{DD_9l8MRGeJHO3d3efAey z{Q|>YKC)CVe>WB;e!F=*{GY_=x&fd3PMJe>`X`QNzPaK%xmm|WvG|@X)O#@Mm+H@# zKLh~qnH|`JjTP$}>0Z;DLM&WeT^ah$7X6A+-P>lE{~Qj`0p|VJ=^&&z=@4aLN^z<* z{9R_O6ZRAa=ohE#0=;JF6WG|PV4T~no3ISz|0r|`wq)po|9JOARbUVnu0R6O9;l7W zz3X(nsr-H{I=j2#rAJ{up}oz~+c1-(T)xpa2U@}51J`cafEwCJmN(o5`KiHgnir;w zRT`#Z7+R0gtemp6qril4GRU8!M=)*G!y>+7eXQ81p}T51X@j@^@M|DhaYa)K-2IOh zoA#vSKdyqpzZ0W~LmWb@nfX@XyULK>19=Q+A;KrqHKLmwb^5ygynuOzo3Oh?{y#YL zo(}tL5EX(;$-qNk^=khnb-yCwPbwCJbR8P%j*x~2oaXnC!p>+(g&lafs%SMVMMrg?113m541kf5I zWi%KXRMcU7fty`24SO!ke`c1MAz(#BtM9Q0_-1kch7r;v@OQD?L~7h~FREu0q!UI8 z2#6D#)86;yfo{WJBZDg*QGNL&+2gbDFO2RTr&u@)rbLE@t-PWCB=znDI2O44C{XsBWWc2-Z5?0hHI9+O#3)0_FqgMi`xuAe zF!b(T#ZZ~XSb;qmSXMSV2|V_=)wT8XA;f#(U2D9e9kQ>ePkh5?DS%Rpnh4^M+xjA) zzh{q;fO%b@%QX07_h!o&CXL)wHp1+=vgeeH@~c-4sq}bF3Gdv(Qrxl;wG*T`dFUdcHc*nz7@%E_>|H!&RLo(o>J?m-u(E(S z=<`DNE2dDH4s5>zn~4DWLdT!8nz(dZ2T1{=t7R3q@@5o@qq?EsDm99?*(Og0B1$SuX6W=}9jK;Q&p8YY)Er6xM*&w#xCU9bBK@Z-{f&Cx6 zm1e2xMUEr@f!;_IK`(95UYk)`OY@?_*WS{jq-eX^Sr!_(GOr1z+Nntgk`PC5_rd1! z*=zOe-ALvsV+E&HP1LWt0i>JUv_5GuvO$#`O27?n7Ynuj0|>#jwWpw%BbBjW0(O{b z{R_xD|8IZ2=AG(c9b_o}-YP5=OsHW~|1to7*w{pbcmD72>o!OOmK`C?vCkAXo(+R2 z*`~KotRC2=-7icXDwle+d{0;43GQVPsHoQ&Ta9t~gu^hM+n?R|6_i~;!wa=o6a!92 zi(E7MmfFdnSL^%&rk)Z0(SMN`0-ZA}kG5MV2Oa)B=*Kcj2NDA2zX1|&v@qSYy#iF0 z)8$rVh}!+Wm6a4X22HIIh({bR1fl>_FJZ~6sqx6KqWc;;nMZrE+!cj?U~sn;#17XN zn2>NbpU#~*dt8-M0|6~}h4sO{H8g&YUNoK{*|Cwgl=fjg9?BqRyF zZvMw4uJ2dQKez^s$P5n;W33tbjJG{TUfLqJy`masXJ!st6=Cu56nGY`=2YP~H;xdT zS{PS5>Lh|{Y4+pA`E`$)M+Tp41JQrApG!A7p{eI=`+w<=K#`04_Lz>Ns zU&Fdy!+6*;1WR7@{^DQ32|TdfMqDi6+`gM0&oA~Cnj_t}r}~D%e9m$DYB}J4@A52;N5J&~GcYl^*3XF)ST?tMZF7JZnhwbzQZoU9r>0N3 zEWO>m<4|!d;cxc$oRMw5D|FDS*r4T+j3IuCY|FWA!_12z>Fei!^YYpH?>EkX2CwZY zeli?tKl0whRln>fto*9*R>dXc%rC~T++3ZJ|K9sxf;>Ojw5E-;>=)lC+{Mjt2Sy@6 zcrsEbV-tQtgPx?)hM51ZbE;Tiy9Jo2`;dfcu`g*zFcy9HR_o|)sRN5F#uw-{!AYoF zk9RO88#!F+KNf>!fG+gpkp|Y2xSMlx_^-d9eOV&hc07{m%mOxV?^ACshlK@-ZTxi` z0DPFv&oypl>-i*_tIyNI>5p_E^( zSfGnCc4Ut;|NPH^3mPWC>@iRC8gALI5eFD0N)+VxC~ujh)t?5ry~tsqgEmoOf0F8Z z)wo0pd~_9sSp%sv(WPhyulLAGz89OWhpj*svB2@cN)LrdRH#NT4L3Yvj5e~P6e+#| ziUz7XDwBhn@!rmrwb)X<&ZjgMd&{h@eEoNh4lNx72J};d>D+$y|1)@y6!!e)rQ?{X zD$Q8qM=x+(B>r#A`J>a&f@@E@BI#=R5HLaJW1T9KEGi9~&B1+2YpL15qd~8FwW8{4 z5V*@pdpSiG224yEeL_7^9y%1;xQ-fZWc!=ay?q41KZafldRpXT0zV?x6HNfjKuue; zgENZC29tF)^T)}gdSWHr;Q5!A@3YZN0gM&*&wxzwIV_XomB9!Z8jS;jqL{CyYkx=8 zd&h-FRhXjj`VsxnawRlLNhQU|lEzf4Egrk)eZQUs2@I@1TeF9;<&fb43GhoXdZiG6 zXUVYC{dnF{n$ADwLkynoYGLpm=f#_2Qgx+QUr9a@=P(cfWGorUn`nR9ygLv7F8SE8 zJY(T=(Q)o{&sXnsG9j(whB7`R#plDx-A|_l{o4jn$e)q&+6z}OSgQ7rBpMLR>MRe0 z+X~3L6+Akqzzhp9e~5l3Ic?bLykg4@0jR>dYkoY$j2p=T&1Z>qki@NTXhZ1?hiY<3 z{j0K%gr|{kP^$lv?{A5bqysV_Ay#8p6atpbCYn4N2 zopcYMz7&&Tr7@=DYUH9u-;zTR;MBwYtfpUyCJhMU)N8n}$l^P&Xzy@(bZykOuI3`J zsA=y0XlPOHQJjFF?G9f1PehX>MMkUn!*S{+PchS9a!l%v1_76@I_*WRC=flk#MjpV z5sRY==IcSfJF7^+cxkj?VHiBkxW-U{n&L+sM4=p?TR^Ntz7_xg3)UDT$^kKU)WNJ- zdY&zHq;#|~Xf8eN`tdhI+N3OEBfYpDWr6hc@{XWD-;bt&GYRHdM&TOiWr6nVU26vB zm$N=}0#E|!9KQDhe_=3(Cyq}Xd0DDL-pK5?@@~tmLOizAg{w+l1%9?_$~lJF%e7pa zM)?B2amNF8E@aQpTN)y?gM0itct-}S}WBO3_V2P;-Z_DtBU@iZbbB^{xRDW zp+ns2EjzLw3+Lc00^xgjrnmctv}3-z`6Qm>uUd-NI5-_gf$c{sOhgn zSOw}=x55q9-q4q#s5crfU*huexmR-tdh*iH@+pL51lj6wbgwwtK18EJKw%!|`la5q zb;t*KXRKxmhY&vA!5`C=UqrD&;f9V6xmXUwC{X>DKWJ}5(UYE*N8i{M*Ou6Qr;O*G zr%B*Dj4LvwM=-3wsGvIK$1H`WgvE=mML?6u+rNFkI6sC6f%0RKRjyQl)9>3YzL}!f z17_y4PbQc7EW&bX)MnqwKLTWy#LtlYC=wkp$2lr1q;xhOU)e15(9!_cyCe7)ba}aE zv6-B&vu$kY`oMhiRfS`Ya-Jg51Zi;9MS#gq<|gV~-9WJVw)?X-Ho#`(#5>ublG^wc zprTO({|S4nGSMJ6ZI_|@mOk=fwkAw2CSa+r@H9-KUk17KB8N+9{DxK$kVpz&{@>Xj zZi>eO!OP0hH+Iqjl4nqmR^b7LjLi+m)1+_T%wM$njkF$ItC`ggHwr58i+EH>vIyd% z&Gi)<|68(igELb-1J9*PX6s5*jb9D>>`VG=3_o$VCzk_QY0K%B+Q|@(d5-m?wn*WE{Cr6>l$(ug2qmdcjp}i17Plm-mrz*+K#(P^Yj{znlXz*W1N*zt zlyZT-`to$P;P2W}V97;kL1YS_$Q+PSBvCNWckV7omhilSSRO!w+YLpq_cm&EB<0Yv zhUmckdo2qFL0X2HYZI-R>It#}An*ukEO9!%3j0MNG7sBfrYpL})J_3dXt$lQZ1Fx1 zIB9IS=_+W(uh~%T>yG-SiR11Q@J5cG>tf4=?xY#+*SmRPx7shUC>u}yv6@}5=;I4Z zYZmJekTfN;`E`sUKmLFg&^%IBnK{IlM6o4AhV?z~_?}z6-|XI2#UAFzJ2+uh}KAh^AGzm2Da ztKeLQQi~BZwsNbYY9HWc(=B}(!8feH;~uHQizuJ6TK8%+#iVoCSn+kW?`&*r1Xo7V zyr=QoZ5qbnOOI}OC^?wc8rM!`!B_UOKUda5q8|}G70$zKdCip1zZ8AX*3>c&COOMf zR8fM#x7_FWU)L-oC*E&-A*wPt)m)x(`8)iZVOeMhi?SA@^rPg#8Xm}zDrUVG(>CLT z$~SqIn7@A?jObfiJDvGU2PWd<sL)99)8EFOz6Az@eJ;+Qn2a2LuEo%V%<^vBH+=D|9c^ z%{dFfaTLSNK3=wwU4=mD48{?U`UxOEwW6}}u6OC2vTkm7B;YSk7eCHpbo<>z^};V` z{|u#!!t=i^9}!r==~oT1L~eG!-@M9a`+mE9=RSNgN8H`d3+pL$C@&L9(oUVGTbUf|m(x1My7a=3)nBFD_*;;F zPw?%E{V!X_UbH`4o+@Exg$J+?H5`#qQ9P3t&yzdRzN2`G7#bwkp;(Ypzj!u;yTz=& z>M>nTwYp+zWE%`unbnggT=V72O6t$9lx6@1anGX1F z+6&V83|3~0Q%8)E$x9pD^LXeJRhnJ!O$e%D`B@#tX3^d}PiAbQf~=c!N?g4>Vvr%7 ze}2eMR{Fe@`vOvM;`FvR*{ZdaJ7i?|BigcBGPuTw=xt4__M}a{kk5b|Y;IRETvt`o z+yAWH?4pxxJndg2_-iMAaa1}bgPC!rksPGv>m2%WeM=RWqqCheXZ!)BOW!DU1sc0v z$QWe*cMiu;1THr)QrCC`JuGM>J;3KUp7tt69X4 z|61g$Vh;K>xYqf3K|=!D>?!hf3d{JwLNkW`Ija&$-pZQTf0D82 zIs`t6=YO!Qv?}(pSq4E;IyEo!%-yTiK5Knzi@RV#m^TyNyqR^qAblG+E~NEQ(sKQ| z&BL6J9|ey)y!)_9eSI!dbb2^fwM* zatXh>nw{3WSzXQFzmlLnez(7>y#1!CUn8aMs?}9gqsHO!!zPLEZ@7IZDDawt_VK_^ z7#Wj2rg?i2z@|3S$FaF)nKM?nq0idbXH?FI4|s3??V$BvRZb)S;h2x?nJ-ESdS120 z;wR_lI4CUD!c-HB{_==3tmdFm!Z~PeNc_6dd*lPsKgxX7xy*Q?qbb@+R zj^s)|iCi8MU1IC(ilW5jH2EKdut~gpnKt&$?D@#>WMtPGoWOd@y}`5uhx|B^{|=VB z7F>MpRzomtH&Z5%BV1xa#tKH@_@d{ya6l{9KN)_x`9-OAYP@EA*ZT?wg^RZwYL&Tm zFEz^LBhdD^`C2FnmBOY7jDD{((K=;6qpIrLI(Jf=xzJv)eJYV2hvucEm=CBmM$a(a z9WrIW+ip+4I}op@dQ2_o!z%dr9LonQ?z7pp@m~KJ2^mpmp28baV|HPH|q}rmz)sUXHU=7ioQr>7SgWvAr(6`{63_*TKu~fa&hM z4|H3JTbc@s?~eHH1n;A$*iI+wGPd^v_WhjeuT%E#m7b$6FHp>E zg0`3p@7B47_y2RNJ@+lm!$o zl{j-htNcGLz&i;I0^&x5TRU}%bD84@&W%6{-@mzh>&emL9mkw;>r5VLQ@yCPtIn!! z%Z+_8#>1Zb^wXvLxSgbW!G$2tZcZQZ<@T|3^h&1DuOI05>2l(>mBv?}x0~AbajWV3 zDWcceZ}@5cND{=ANhrtsd&BNtW9*>i;FJY}|LNum|NJlIjpxy@fiCBX9b;Ga4A1NI zI$yi>=pT~D_tuZ|qh%f{ef0ZzSC)EfFR)7?$vTcr7K0YdMB`bwTjr1Y#Ej2{EeqtP zX-z$A6K#aNRxeJN?bgOaL`mdH?87YF5_{v2j7CPsRdG}Rxi7ymuw4&q4t-m@oAVm# zTnT9mDfZpoxrodfaQI%^+~uj4xPQEU6BSIjGGTN4t^Q)`Y34=e!@4swtYxsE`~%!H zGHOh*z@`(@kkgA_o%N@~HU=TbKpi%`pT}lLTX2G`mY4JXRrMS^NEkTGXeq`f;<; z3;cVf*-`B2RiWg2;l_?*TYJ6M{;y#y5c*waL;$t4XP(`W1(fi-JMkHLM6KrOFZr-B zT1EvAE3|FNP@&IWFp)SVz^U7tb){!8v{ZE8XSwC8;j_?~M{1G&`A7MwRwMMp5h*o# zhz#pzd*<`pk`0+PFVmWvc#0)dCqEd!@*)vb5t92mnrc*zjSej&&iK3&>RF&4{#2kO zEpr(u%ue@X$_x?fAiMLdI?NC81l;BulrxSj3z%flFUEDgcpVI6I}hiq)5rjR%9?+W zpjE8y4*aE>W#5d{N5uMqlY&Dp+bDn90yf-6s7MkHa}3bCNw0|Xu2>8w#=SjKutRZd z&}3!EgRpEW{}y>~4q#u9z)&wkzxC>5dD8=(;i21I_Fa}j4=C)Uu)%Mb4zV#l_g%fo z7WFeu((A)nl5ZbQqPa>%s@B$NG3T@jZRKKK3><$$k%1b_OGS7Two-{X$Ax-J@|-kM5L zeKw=JE~q|*hZkcBOC)bDSGm9o;$q5Nr*kaS?!8}(C2to{fbNlKB<0-UDxlrBQ!uYB zx~eShr?#YEiG)B*Ri*>`zx*9$$JFH(4!*qLgqKd52jTppv@f(aE(cFsgY&$dVBNi} zCx7;?ihB)V?Tdb|2<6tl4!6a3hm-Zhc0Fz9pIZM-@q``1y!=uk^e5lG{W#tS6gR`` zU4Q?I?0Kt~sx+{FJ11Ce&Ar7TBlDnRaw`J=nuS6|EVeY;i0h?dFB#vX`=86`6?p8t zL=yc9$>j223_Yn^suW93-mj1OxO3Q(Ufj#fCS(0^Z#|hR5kCGPsdL$MXh@xWy*BGm zFEl4tbHj}a9Ld2#6mIFVXTi#$-|0}ReR{U!r-fi@`$q?9^4T|^_S(PM>@%Boo-#q) z9AyLzV?Vz1BhEw2354TTzo68)SC)o)sPP9Diy%3k2Sm!6Sp-LG&v^kfHTSZw zs(6SwU0`g&ywKc_X2Cc#rR-_IoBq!_#G;zoY?&C6D9*r4?}6@-Ly0pLJ$b~Vswh-4 z4gymtZM@quCZ&<>w;L_b+%(L}rr`*4EY*Y&oWZ)IXd|V*6`1?ogf!pv-3=`5_^f?0 zS<6`pI5YFXJ(}97yKCtyErnHspi=|%A<12zDEr0ZTwhl9=Uty_&CSdojCFK;9U7Vr zJgnqD{+rSVd+61>eZ`4sa%wKoA1Knwoi<+b-M}%pDeN0%Hovl3aay>rOJqGVIVGhT z=IQH;xLSx}3f$bIWRwI{89qaHZ(B4Ty%bKfZ+*R4PlQtcnWfm1f{Kri2b{>W=Pj&o zBE7x5gavi_GvTY=J_(@r?Ts?Oi>Ws-!D%~i0^oa+apj)LrLID3>^KP%K?W2+Ki{BOsu;>&tdGjKc zB4MxB{5K;;cA{?P0|mt+z>>Ix#qdEs#@2SDc51z{oFRp@#HN||hXZmDZvk68P3N?UH=V>uk95Gi)5I)lOqONhs z>itx!pJd_b#?_-;O1?McWj5b6F{+lc8aL;S$K2ieXR**;qJ6lM&*F`HxKD#0BJtvH z4YVQf=#UC8G@{BbW3a;ck67_~v1V}{E73uRQf|Z5acpTSJc2VR?2Eh)Ni2Bx6281) z;0S#oE$!3!_wQz}ENpsunxf_Qva3B8>s0?3*6VQUw;zDh7>RrW%R}|u<8^I^FWccIY`eeM# zhwss~owfS%{gm9nW?ZmuGfq~Nh{>Kj-N}lv>M8|5VQO(7XAO0QMtH zbI{4=&Ap5EWp~+~Rfj@OpS_sT(@7e<=F>>DK_gYtNZUV&V9}*(hG3k5Jr%x`k^1xW zlpRXhJXFNM9c&!0c*fXd9RuYFS#;_^GoF zED8ykU7i`bdlrLq8t8HAbbDQj8B!=g5u3O%i52fFsqg$Xwr9B$#S&^d14@8o^& zWOM8bTe6JuQSYP0MVI>Tx6^m;%1^VGu92$Om$uTbdrRlNqVLL=uGj19niqnumTt~8 zuNU?MA0_8Xz8L!XtgLJcKEAey8=B{8 zv->4+e{{Y@4^2)s-g~!qnY*O!x6pt`*)kC%aTksPK8VOaJaNr`%b^#p{8{nEQ|0Bv zP4aQ`g~%j-!AG z?{H2hl>KAI5UB^yHWm}VLy`d|@?d=wJpVL~{99%RVu6g5LV!Umn@X$GC6hbum>Elj zZ0m->l-2KHaSEy-r3;SS%iGKuAx&)dv)a@O=P$pq;M@fyzvR7!p0y?TS|THhHos>! zaM3QeK8A#%6^by;k6B`Z#RvR{4@;lw{`1O|WzPDMcP#pK0N%Oa=#^v`lMMIPp1^d% z`aa-0-q)iHXND-_%f20X7Vc|pW63rO04HFkgh<16A}@X zn&J>$rnV|SW8)ezYNMh~JeV94Pso*2eQ$wdJYLNBnMu~U?eW_B%da}XAOX&pb*oI2 z-dnDS%%M~|#&q8CB!XzV<Po1XfWwa)jAs6wwp@vHbHQi9x+v^l|G+MA8;rEG^)FO-^Aq<*S zFfk#~O=0I}ns*^&^|W_Dnl~o{xu5g0(A5JPEG0s^c|GSAUMg6Pjwq_O=CBIoNRqg4 zZ8YAr5vchRxj+kc*u_f}qie(k-Ky*qK6BcOpA|BS`aGqYk7O?(v-1~>V9E$ad8CiN zC);Bs`|>Oo)olBRdqP!>Ys4b*1@=#~TJT1Z|C6 zc?h}XO1_B%;wJ)37g&rsm~}k1@5BR%3*JjEutMAn6fdzz-50@=z z;i|Rt&}oM*P#o#Xfa{kj`gmDLqdse1omqoEWB?MdL?AKO+aLgTz3OP!{PqO1O8CIm zFakLC&1XUa)My0!nvQ8r`;>*J_XNJ(o}(wc(PCb?;V8?>6spP<+hb+*IvD>ujObTc zOd?%oNBgI&{nb=kgoq z+PSs-%BFyN;60G@GbTMc*2sT55$~i#$KolwS2?*t#K6M;gn?No~9e4kCHXp^_(cZH+(9zQe$B5aS zi7~Uw*^#^#(Byc=B9OJn^DBEkxlL5a?LxJvlBlv_Di@KNy%E{C^~tTWzVH|0v#~e1 zvn$U0FIK;>U+1*M6DQDWZ{&xSQb0V}xd+u0{v%`f;&y@?4 zyK^Fbe|j75O1^GIg#&5tFSIe(3_(ariIR{mpNBu{OCFS;^>;H{It?$uSz+=bq97_- z5J2tbCmt2Ndktb`0q};5Q9}e1qG}3CKNF^9_wP$PuN9#+iw+oeTvfpBBswg_`1mm}Wy)d&j3S={qMFHJPvo0f$5bB|cj%0hP-D+wryNNrd%=%i#s zf^uSd#}?euvP6;y%TvVTAB92pdYV55a~AgQmf*Z*x2_-c;i*zI?2jb^O|XB%pz)SX zhJ>llyuzqV?zO1@&{Diu1cVJ^5BT%a-gD zG@u2#y2x;^6Qz{jUuZ}uGJLT%v4y>Fcwvz#9(yX1=lzF)*))=pMGII>Uf(G|MzqZD zX-0OvNMA&ymhpB2xVSN+QH5C}*d0g_(oGA>@o`*h0Tznjqg;f9|A>wj@ zh5>;^#Ka;d(Q|DC0;R~|znoTJ`D5(yMcK}igz_0%HpFUnlQmz^&Wq^ZAnxtW9-a}H&nRTx zfx(f{tYK**%I3dXoaxI`k)f@g(KwQWi(#-he$2ISb5Bc4$c}CB(YKuq(<9Y_Bt@qn zw_ITqoo2dq=9N#o$HkW~SRl1mRBsyVrAN!H``ft-$!TXiTA9EZyE|B+1Ular$<*3n z5T>3yt}Y2Q7xQ$Ho8?p=M7><2Ku^X z5X19fuF9|U6xoMZy&u9#ytS$?nG(M$nSQZX@Gh2)j%%^qp7!MWas@rLpP@`MAxqMI zIvU~)9iZTHP#UB5p5TmeYHHIUgc~YSM@U&M1Yi{qS?*dN$22>0DU5|%gB5N9b}>Pj zE^2Xb4}gx;L?&ay#K{LdVbV6xWF3*tRmSH!HuTwm zh1Rwg#Z@&8HJp;_L=ifs89nR z7qorMbAN%b?u*`@;2WT37)7BAV^uRZfaq6fK{eX|q0Sb~so%sH)g%*BQ}CtS@9|%! zb29=jJOc$;z)@DN{_j?ci;H_b{QL+mhjLc9sw%507yM`U5u5uHi`gee_g7UP2X`F( zil(oJ*y0sAjyR02f2KvDJC>5|$;o0sVJoZ1d#~1!=L8+!xHk9O2EF&TY5mZ2GqLCk zKhKf49#g$RualGT5xqHd{eRwlSKWB^`4me))DumH{Kp2Zvr0p83Ktg%3ke~3oiE-V zbQ;15Pd;Bo*iOJPMN~o7+-#UPIk3R&}5q*HcwUWGPq6el(hdglf~_^sp## zS3NuMyE)LFLi^aV<{C6TxnEzCCaC+|K^(0RHq+Ok7fYTO4_gPfKj8hVXeqI%1Tgt< zC~;2@plzw(zkk-Nv>wCP{}{ZgM%UUpiDL0Wrqm*9_j=qzGkN zVQ^>XcndXL#$KjqCNMV;*%JUbU^WG8%$~QruNpyLGRlNq?fu2?^ZdU*Xd4LxJ}IE& zWOqye8LxgGSLz$UbF1B2q-oWGn=2m3;2BCX)%^&(@ zW0F4975)gFc6SDG1pQSFd=dH`cm1g!2IFgWE?+VSR)&&%1-eFV)fSb3jPF=W&zWk*o1>W*+9u@oH910Cd-_+O zjxGWPLh_SB!1TqNMC?O?phx^ngVp?~5HIHy7(}fmB$tBV8RfG_Y-XkaVu;7CYNg%v zz&qyf{oI_9Qt!U_y+>{~Q~AS-+DIPKGX!dpJyBFXefH6=VUHWMp4544P{ttSOzMyw>VHZ zT2A&R9D!>$g@e6cllLNQL%rpE$7ZfU9}$%YD`XxpvW5=>@UJ$mV?8Kt(D9L#^)s8 zJ&3R~y}TeAv2`>Es9b0ZAA!NsNd*=TUe}NAl!o;7X^MNY&q4MODkYk?mbfrEBW^+&Sy?=fBu5 zg~<_nTmgNbaDHVM$bOx6UXOSC$itPiT7q49)<0=6CRRwUi(eEb_VITVPkh&p?30yT zvCZe*#ZfYSZm=n*t7BxT{a`jCq9ZL?9SrwU3+0;~daia*{RS~>!<%q5c>EsPk=K15 zC3!w5b;vHq3~r8jPzwy$mI2`SSD-Cw#HSg;gLpDay5=of!feWFs2J)k_fO z@vh@Z?56*AIqiy{oriTM=YX4zJ@>_j#t3`znVeZIL@7J#N@4fpC7UjQkGj@K?q!b| zcHFfY`1CG${eFR6eW1(3AT24Zr`w~|edf$3CkeYYm`(HpAY3c|MN8@fGIIo8%bc_` z8i#20STlYdUi$(%o3m)Zjs>@Y)ffM(RNi*z9V3_u_9A~z{p(SX{~#(dIy6~*JzIwq zPet$P>`Ry6ST6y%r_3nhiQ3r9Cuu*m>20c8=6H%tT3@v)buLbc0I4o5zHVK~*Jtjp zJN@os6f1WUCMNG!{(WI_HdkU@V3BwQ4qr)94 z0?i*{UUYQ%agx15%WC>7E((TGMg79j2;C2-3z@ zh$on+=Q$4l^rg^QPlA1c4+eKadr?~Rjn|wX%a`5eBALa-0#Cyyvz3*V(sKQ6BE==& z0TM1~vIZ6-q}3PBi%#;*11`H60wq3*qT%43ck}!Av5vQ{@bk#P!-^}nzzxrU0fWH3 znSj{8M%Q!P=;#8OaQjAmu+QG#J5ElnyagqW6r)RT z@bzX-Jgoh|H%LWAMZ*S#`cqE?v4XP>R)p=S3e4ETiTGK`&$F_5 zx1ixfDPJQ(!V2^T@>o3CKHhuoPF3rTk6`-TSziWH&Zzcg>@9DdN~fsXg*GYG zdN|qN{rb$-{zmj2JkZ9c@eQj;CJ~DIBK8DC??w-*(0bWLqRUl74Jjf$N6S?pRcJ=G zb?S)4ShE2aHX4$O0rGbDY-!QU%0RO^%P@3a8+^k@mFu}*(wsy+Ju*?{ z=ctF7UFi-=p>-Q&M-`FJu$@tAV~7u_ZAP5lrDl-fbzK}RYShP9F}OCpp{D23E=>N^ z`W8UUE*6lA*a-p=`KB(2s4B9x-_LnJy*=Wu#bsi{@v5C`8dCL@aq)E3AW6i-12rdJ zJpqkazf-N$X2?XH8VGG@zjbMmmPT0{R4 zkq&=AQfmxXR4Yo&yYGzRbcH@9);znrkYFtEz1T;Op@{f--8&DbRqE|;BA4$Qv>4og z&o+A9N*)&r=Fx-^EIoi?X_P2>KIV5nI^&u;^K9vHxvf6Tz)^sb(vs~WF5_tC{o;C|ElO&>_47=-Yl`vmlRLv|x ziwgp<|D2xkU*ib)%Ui|Oz{vv+Sn7XoZJn5U_&E(cDieJRJ@34Cd{ER`0p!0cO&S^* zHT(WkyR1C$h0y-ea7dIhsjEHF04#Vw-j|2@qVk06%7;8RwM`5i|Gz8(@3C+XwfTt| zztmqoW>)d-n%>Z%ih`Lu#gdoR>C}JaV*Wn=XL^};T;liN&`_e(i$eX`BFs^q8B%mu zJk_17`Zkyw(AfB`Gsy(C72c~~8jxXzgjzd%%Xx7x80dYa86{mMCcZv(l74egM0sFI z=Nb|^oBf_1X&5^7&loFF*sgBdf6KSS%^`g}b4x>9R&-ONhwbF*j?8yW>8-S4U zOX|o_CJ7?_zItECeDD}xc;X;f38-mm~7`e$_;<= z>45MuYr0ugxr@j}TyS;Ym!FqvwiDl&R54}t7s*a<@Aqae$R*}&D?sxp1OiPtSt0I( z*&H<1r9PMA0Q1+kao!290!p?&F=4>>`{+)ZV=ue0F^_tCysVG*Rdfol#|LS7T#Em# zOHSB0$$R|8uR7c=It1>#_$sww^DBZR!k-|hA)e91yNTQZkHFlvI>b7xty8OC;X<=1 zUL|Z)$J+0*iGQ8xcc;z7m{^|2d_%n!{Io|Pogw&xsSQbP{09XS1@wsemlJ+U3VTmq z(Bld7!29A}F!TAR6DtSM069~YoE z|6x6M@je1P)0c4k^%y(dr$XyjnGb9FCm4Ov1l5jzqGRZ0|Bc_T$?IzZ@oWc5omWn9cL~|u&gb`h@x-D(XYK#+=l;Q^<*A)h63-JSVsh&*LK(+B z#dt&u0Y-#D)PtIeMjk;?2`x!Nvl+7PUB6;fCOaggo+$52^V-SxW`wg>D<-&yB_W&; z2c^yWA1Hs%_ij`P3xFy!7%hsKo^eSQ{sFM!a&*N+o zeapaNSx{95Gwli++Q;~VC^#?Z#2s1Rqn^5GNIfVJx zgbI~{xb9%^wM{2iP9k)GE0-uMr*nAj=c?m(zo|u{`78>nHjl_xc@#6E`nP^x-1)Vy z^HU8(i6yw654o6SO7x?6O-u`Z+@>_eVe}Wi5#bI>Y#S-H&Q$-k`1gaY1){apb$MoR zs>sSkJDQhOC`}z061FwUV3ooKC#dspa229`7|m#>55I}$zkM)=j(_#RrYiHhG3+lv zPuJ(H$R6`fyMLBSELGsxRh{}Q^2jp-EB23*%zKoHlAv<>CE1^*S8mLZ-j53-J!D$= zudCL-MEw%eH+5fCHvRiv>kVsR?c9UF2cz2w_*!Z+4CIQuV15RU@bcrX&>SJJIDzly}Sp-TMFCko<2+ zLd2TN6?+m$@JotlW3iE`bq*aJ;RPp~;nm#!axC^qqJV<70m6B@=I@e94%u z!KxKZ#wLx4t&B<>C@>8@(y$WG|CaBSzeL%;ShYzbtC#)Ol4~BCABZd_iLHFfczji| z@AUQ?hFswA7UQI2PEr#Y<2FQ5mMlrB7qot)DPzi-%cjUd_SB;AmkE`YXDZ6g$xUb5 z$=R8gpw)ybq}5tW+kosA7Z_O|=WKITk~BmT^&z$^AD1ZR%ml|=pUFLcvgT#@x^|JL zgvkF#?l^t9&hfSthFnEow-Q@@>UN(;CGwx<3v4%+jo!hh_FTX<7)Z^Rd40g~rSl~J zLEDK8yrZMT<6>J`i}oFfbz1BbTb0n1_v}n_OemCr1A8IMxSH#54@)+}#ev6qT4k5P zbH<{TP64ANS^7IVZx)mP&Liy;Pcu@$z_3Vm@@r-5j(HqAgJvk~RkZ9U$$Ak|h1f~z zS38gCqW8O zM7~krkWe}Y^nCj6bUq7GgFnJw5P~k5UZ)(o**MI}g#YarW{HU!rylQe5&Fu4izp2-Z za)Q_q@$UTGIL46EAp6ESbZ$*pk?8QxL-v!D0uSA(ma!1hN9xG9`^kNm)G85pzhICj?ysSp-G5{@J&9q1c``WntaVjJY2V3r6Zwt)6k+ zZfGFgJG8`~sW@Vd4BY&|v0}En8&oZUj$zn7hPwG1$U;dmnp)7IZ$;KIIK^rkYr%xWY`=-2BWtn#r(gVoSJ=kr8R)G$)wz zA35&a;B-q%3+C?SWy$pKSMyZ(@THu*A1ev1*(~&V*-&cUX&OkH0Wo;W2A=&%+o;H* zgbAef*-eBTkXgpkX;>UT`0ZA19_7d0_i^9DAFNd|5cwAFaj}U&jB$^pUu|#yT&!Ov zL`C<71et8gs#m}eze9=stoVASvj`?<-QHe}p`LYk6d)h(;dh;EY(V`s#;RL8@Bg?% znB^6=R^la;IY6KFLRuQpvb#I4bc2(g87U#qPv`+heUa5V#`qgIkc9&Xa#Tj_vjrzN%dE&FXIh&5_7(d z&vk2d#Ow^t#}r}i?iX8=mit|(Khi;9+;MWOkP5~q)x+-Rl973Kox-QIm$YnE9%EZN!N=;W+=o!@GkO6!8MS&o(G>8ER?yB#kkk!6A#vY^fY{ zw0->>g*HOi!zy-Y)FiGqnZcwmvtcmFtopNGJ%`uVH9IXUp%#<;Kl0u%7kC@Q9*0(S z-ctjY*E5~Zn!GM2BB6fmhFD!ImK%tsIev->kJd-M3vS5g`+Nhu{aw5wg$+pLf6P9) zmbNBkAKndODQd%0Ow%M+dq+kuW)u6hMHe82MwB51LFlOP~W?h8~F8((cNoE zG~MrpH{xQ_y=dVZGV@K-Ma_%LU7ypIw)Ma%m{oyn96XSP?Ap z=1vK6dm{00g3P{~%e$NN3s6m^aJ@b3VPO78>-Y^#$6SIy^ts+Bt@SDqZ4y*))d&3+ zC{-N&dn{z(eY~oRdwO~W0WSaCMZ(uzRLi0jVXsC=g9}FyUJcFAh}RVad_mzP(>t%h z&*&BuIi}8nk-2#Wo(7&SUdb9aKkp_2{4649!QWUrl~L>a5KQ) zJ?gb!I?r5i8SXDbN&abfoLYeJ!tk5y1yHwRq7st5MmO8qGHMI0fgvd8Nw=iNd95yV ze+Y-~jP{?Tp#18DR^=a`6o!;@RiRkcv|wo?;fRG+Pht?-wnjY&uLT!)!}@g$3=A;3 z(2O0VjTY|;)kko4ZinH_nZ)I_BkzfyIOWhufwaI+vAmfR?>zy{cR%}1AhJz*RXjY1 zf@(xb>GPS_*#B{Iz%D*M$RG}m&mQl@@`r-*OMWjn2-*+t9ZldN1?-h#mt{q%d$NTS^G2C4KVq&he zynASE`uL}!W-&48=R>r{R|lW3iu)^jD{Mx5&&2$-*_QEmR+R~)fY~);yNiQBstgJ_ zoY&x%j4_()fui~qv9`4bW?-LM+NzZ@QJxwgZ_kYA9{B!C6+=FM{9Z$0X`{>PtHhA! z6+A(jMIKupP*4S&3S;x@`IQQIfCS5k`yQ%q{<}RZD;towbHeruEYdSsv!;34lw?eE zDIidt2cJ2=dt|{OkiOttm{_t{b)g*ZZi{CBT6RUSj_RDl$st`=*Uf+Ij z9b44<(_ZmPR>48+$y(ZDNp35Zyz3^{uAI(+JKKw$#*Z`o3@$#w1R<;Ul_96$!Tq~9 zgEC&#`A@=ICkl}+K_nqaH;z84%u0@r(y&jf*0QgN&SnssQz$zb-jm93zL=3XwZF|J zOr>Ng1izASGpTt>0A7Nr9OP#!=&nR|x8`f&$`yWclbb+g(ZHkv8g*TK>x6ST2sll} zH4%)4vfin$?gkTlwxH11{wVdKpxku+IT%0qy)0BR2>0bjo6v74<9H^6x51^jC;~j3 z4NDk|26^1>%^Q|ni|i+<)GnhkYu&?P4t@$UsQ4cIy7O6==;o0J!5D1 zUO9_Op`tta1KdA7633WiR&zd?FeZ`XbGoMc8d&U$#Un83nO7aMgo&&S4#y$KfyD59 z1IzvVvDoJ>=tRo>+rw)0&HIy_m6t@rJH{J-?#3JDYd9Chx@4ye7?zJT9_tJm09 zdEjC%xErXEqnuj}(qO$yfw<|yg{e)&Nn4V}(sdKRdRs>VK_FIl-v09E!UN#1$w(Wy z)vtDJO{Wdo%yD8(;NYF-SJYGP_FBw zal16uD!s!YBaI>E*MlBoQAvXEBr8;+@)`K3|8;b?Ukpip7;JvB?}Sw_9X%wpAW?27 z!!HfxV+l1(QcjG~%q}b@I|v(+W@O=Z!U0$bg661wd+&I8?bet7XIv>ae1$ zL!q*p;Bx_jfLBmxP*Hyjwt4Ct2?#3*pK5a!Zz%wF=7P;FK%CD8X0L;;QZWKvA*ob| zcds!J!6m2TC$oc8X4_CGu9_@@JxuyF#8>wA&@KWaQM=hB&f=FAC=pg+?UUOr6nM)U z0r0inVz2aEPKY@ME)nIES8HsH8%HHV@a^gZ{P{(h1*`SXeiGhe#1 z!w-xH@L{<=1-P(l^o1>W)G0Rm&YaknF3eN%6xoNfT{R1(G6`aSH^{QUEMB1AQ;Cs*MGtl9q+U=qZb9&@WLLT8ZRP@fmy?KpF6ZW;CHf?jr6zf0Nf|^6SLSdA zDMJI%W@(PjiOu7K`Laeci(WFv-asHSRUc>~LF80^ zwtl(Hp{3|DLiYKZN3{V!q+U$;i6)9q2bVzD7B_sA&TMPl5C95zQ@;!h zHEpbfeb`JrL+VYr&(x2ZQWKxd#RY?^N1`$)K7XQ$X|nc}YaEkE zH(wa=erU@PeD^|z<^m?g==S)Tv`*P(Ib0WbX#(r)APRGWdie_z2(55u4 zd5a7;`Lav;Dop&@MeeWpm8a{Dm;Ma6K}priaSx}==D9JbMVXr}-m2{?H_<`}7|+z}{4luz#*Wn8UX?lLQ5i;kOQcIWm-H{P@w zZxV8U5(P_4(+VuefAqsV6lq)I$e=8VT@=YVN*OxXp3U7l@{|xQV2i_y+c17}i-C|w z!TW*WiFAO`1{>JK$n3aiGo7(50P*&lsv@ho?4vflyxEg>3Roge_8K^uA+F^`%zPm4 zgb*-jqlTrR#Rg#fVJ-xE1lBlvBJ-ntOoIZVk=S5A%J;d{!*tcyIVLe`0tQ1B z5nnO4uE7UH3=im@lW0l;YEU=#fS~F3!iuX=-LOL^YUG{tL&DRx6jSKqUM< ze^?1J?w9a1YQ1d#QGo|flXKT-A^x`>LX57_7hqCMJ!5z!G6Vtm@)l;d$ikSvyEOBU zpt4zulTj=rb^=+R_uB?QI?NUJMpP8iho%WU*7#WPnM4M+bf*xGkjK$CgU%%L3`XI`hJr4@J^E|=~6mNf%EhCrMn4dD%l35>UJt{o)j2zMBjwAbw}9lMy< zVGaB3!NayF+Wa-w*No(2hO~0|8KAxC-H#a<|5XKC%@~i{9i4U9)1>bXArP}G@nBON zXc!Z*<5>%A42HJEpaimF_2+t&GMZ>x3tHb2bnRn1;}lcH){dFD%3&_7RrbHqgaUQA zAdrTF@Wzy!hE3-=S|GH}rt(ZwAsXRs0RFfA&jl5cxh5WI(j?XtDKE@ zZUK^MtQ+rtUwmPv;9Wpn{RVuoT2 z`p#so1pHwpmDb@TLZB@UgO3+c+b#nC`bzGoEhf%<-rPIDMq%!13_#b=5C?erYymaM z9>G@(QzO6FU&KP^nQNY1`(%-d`Fh|0;huywRUhKQ@m1N;b-uGzo)|Gxda-L$x*@)O zQmH$O+aR3|u9VC9vQ6geQ@fFQ-E-%7mzZb!3p}Um5dX|xbP0``c@5%m)qM-)9QlrF zlkBxjw%oa+*r30@b*sx{6jEU>{KcL_7jb@jb;4Dw3hlh-JF%(VZ{czQfC+MGFQ*4`5W|~$RhGN{nSdN>*R1v0K35;Z zUsF?S^$7Hfl>FbSYea6RNCW#dPs*TtFK-0N$G-0DP#1bq8=v`Q( zYVI4+C*&GQ1?<~COuDXGu=-@zF>UQv=yrw^sq63Mh5lFH-^TEk*vvPeSH+$5(wAS( z^9HCSZV%OP|K8qaXXK&%k3dz&H4^z$HrT2l)8+hXyx|oX?mpwmRPgr4vy8c`tLBWW zv5Zzj?|7a|<$3;CxPUT#Y`iN(_nvF9 zf=h$Qhx5R@v=@ink;JlJxfB3-jtdUPAovu}wK5Qo(Iu-o%JOuV^61LeU5&2eYT6Pu ziMDr*R<&O%{P}nFsWzDpw`BX^;XV+FeEaq-I(OGn#dXXA7PVb#`GW*qGGjqCHO#G1H)VK!(ye1$G5r~|P?OCt=QQLYG1sUWTsE}&bE?4UuZgLbxbH z!^{C4Sc;9@zQoqA*lJcThq%OcehMG!nnW5t`}47cAP2D4U1(X6x1ff=;li-tdS44f~<3>X_gs z4034H2b^N2aB^cQhkP2FE`8CQi}{)u;KfgkcCpcvT8tnFPrvS7c5Swc8aY@FiV^B* zyz|?0+v3-39c|GjNp3C7ibq-!N`0%=?@c7B97$vHT08qd6d@1O7x%rEOO>bE`BKnodkNz zkTYW!L@&+A+Ggjb3~;;1Ws?v#X-Q|aioE%jO8JdcUk(~OI3|n%9tTF?vwi0$A?+eT zX(}C}4)wZ?2RvZ4jag*yPkCtQsRS4klLUF5WEAogk&(gvk<~~e?s~6d8n@cApy{RR zBtKL$OuBaRtGdD*TNGh7g~X_u>64g`1vE5UUsJ!$+1;LIs7(o~ONLRs3qpk8J8_a* ze~4Pa{@l$RmI1pHS*zd3-Pyh&oA3enE%WTm3Jh?t1hPK_k$?u}PUL7lpoUG!aaURGYoi zg+@w-PvoF)M7u|;>)7#Gilg_0umkY#gu(dIyeLTsWOq_J7z=DODYS+Opjw)&Kgm?F@CCa{F}E5tEHkpcyAgy z^$r@G8Oo{5@)n1LH{R!OAilD2ywuu3MpD#q@-gynW>PVaRz?%J2qiBb>*-O z1m(^iasgC~A&HBa$NQRV#7HCVrNE*DuIscdqMrKrq3J)^F|Ah--)HNd39>+3Wvm+) zwN5QZqzRI~uzW?8hXC7F4T^abvTCWk{Z9W%DTp>nch%J{5-xq-;{NeQz@&YD`VL;@gHRtbvpAvs}f+#IS?j{>UYrFuMPD8sFg z*icj#m+B)s@q?Tpj5h}4fJdgH=vR^ji(UgC<2L!>&#S+PHR6BJ0Fnw}2phw*xs@m2 zpLO=rmAa0IQDJNg7O;<#%WvZ9buLyY&f6;Q5Uh}>Qr8OdN1rdM3InQrUxfEtEQQjO z%Te`tgWwTf^M`aG3PO+>I^omZfLWiD8AvX~`pmRZK21A;j<1!cHr z0{LG>calmU#a2BGSWp{zuekz;d44Ml$ZbXL_FmNT^>ia@_hsibE7b4y65YM!-48rS z!T!q?y}|dc*8_E%R;!Y|)%-Dr4qkmhD$R(`M802Xg2|MO_D?9~n8T(>@Zq`ioD`qg z;Wq0nCs}Y&>GS_lx~^^+!w#qQ9hv@#9<;6CI;xQq4fzmun91Zmd6#4iG__XB^nOJk z9O8Fv+pOn7uwUJT-=)g*Z+K$nS{FNfBD*3*51kiQ*;blF#rro6xsk(kbVg;{1(mjN z&JmloeW76^7}0g~8Hb3*DDs=00fuV#duR-b?i)L$#tiZ` z^A44P`8@UTYz9#z+8Fkc_^~dSKcgAr>gb3U5Hi)LtUuU*|E#x>H5IQpuNUNt40pR{dg(JlxM_nODsHdx))kT?-k`i%=COiXP4|f+bpQol; z$_w;;IMWotnWImcQU^{RIi2c<=f0R|K*&?|H*cF-jakx|dd;>*zB+o4dMV#XO zOYku_hv=gLWFGGIG?t%B_!7%lgn&hNe~-zx%jG{V2s*48;T;E=-bj@JgEA$97?3OX z{)~GL0^yNk4)k;dk5hV4JOSeeA#_q$OMloHR5SvIW5o9#LpBgs+b$KzvMD(z0W|=x zrKTd`p|NkwHH^iz(oG-qWC8QkdFxvI7tHFaoJ0(=?k_G4SsRM`_n}n8Z*f7| zWw7bh3`p^^L3yR@uAYK)8a<8Xq}n^iMto1Wvx0{I-02}UQy9(qnDOiVC)l0)O(Ht$0=3m z8Ll6052mj5V8W_nbM5~+F46)qfaRN|3ncOyrSFK`?*3=BXot2dwG&*f9@XlkpkGDg zc1<~8JntSrS=?Tn=l&_ghPuc@)Eeo4(vx>W&a% zBVrPhXl0B;C8ncXH;e(RBn(OG!KSW+DUUZQTPe%-O#w_H%ifu~I1rv(F*ky0%H!Yv z_hP(l>*IfrYCk}Ln!r6>8e0dSFwisrOe3Ly1%~U`2QK3)a_df8kNc4vsiSnbYFnq* z@yFoSpDo)nv585dD+Pv0LU57YN$LV1sO-s3*Beqv5Fm@qjFTe*KhR*mnH^9eTMY`q zDz)QL$M0{X|1g=8bV-=<>S5(kb66hviluXjE1e?ImU0_UEN`qn;Z0Mg#6C0gwxE6g zq9fJ#)}jf+jC6KbC*SsQJUbgCQZgn^!xHv-keO@ivMPfl@g)f;5xFY$cIo{v-}dJ( zYs%|ff1JvEF^qfLG$E%?A9$IM*U0C z;(WrB8RPQ^>`bMNiSM*v17}m0{ASvQ4?c!wxc?m{>D`xRZ-Mt9et4u=|EO1 z8{bhq37}7T7TshPBdd0aVWInx<#F4>sL%$N95<2DzFnK1K^9)tM%R1U!E3U3D8_gU zqrP%c$O|h4xUn3Zy*ZESdF!<*+Ctvg6(&}XG{dA zX^em0zT~HE3KxQS zKbPbES1&qAw~`rcCEZjwDZWS8%tqUf_QXegS{RhFfAfZgAHVQ3^nTtIBNuyL-Fl5f zi}6045Gih!GxKxg&kY@qZ6*#5$8EKKKVh$t^}Gis8$&a54{A=We19ygn!$?)qd-dX z9h+Eg9@UD8r{K`NdxF0HuosouVWuhAbYJ&LKt}*7nP8Q@mKc1pzl-QTi3-qRq+~fQ zupazry?>+dIvRIDmr@{sl;n-)yyb$d&onO4LZb7jzfOC-*+gAn)~fgs#2*B5)zZ#5 zsd71dqdOe>HjdB_m#!xM3%e2wrYz)?G$7<1Aw~WeZ-6&kYD3{sOC?!#wrigF03+C8 zB&@Y8mm4O5Qps)=D3eyel!RMJ!qY!xGLfo0U7R&*9TIT37cUaS25mu^_oq2)-o3kQ zqRhz9J%5|H^(xqghFe5rmPqM9{g#&M#+UZTQGDmxmV9k&`o_qp0Mbk5| zVj-$E!)wal(3|2`#`a^gYJkj)`N>Ui}=B@2e?-n(`CG}iB+Vf$ti;eaXQD2ecB?|WZa^hRQ;ZU$WX7!d$mIaq-BY>NN zcO6%5O!xGb6_O@9il)gY!h+b-nKF&|eDsQq9Qxf?&E6Osz}GkaMX{sDU^Uh>8@`3f z!!@U0^~Qu*O^CqiVZ#TKnx5vR&f!JL(Ce_U;_UVvD3l+hn>v=aie{ctDmP1EvrT%1 zCQ9qNlWVX)cr~*gh7kYcZ!UwIG)nM%II!-+>%g zx;f|2kpJy{(c_MQN*SY|3S!f<{WT(f=8 z9l9E8X{in@0}4oJnq_;6aSWw~x_mguWL5iAE=jK~=qk};j!CE!*$8?(!(GY6JywNn z(r(BM#=?{Ys$waCQ#Mc&p2zKK7wzHo=0Ui6^krtFoAFB!KNyegl!!_i!xsC{r7@88+1!EXlhTngE8)k)wP^Q&HdL_g!frp&)YwLrbX!2 zH%llY?j`;YP2U~PRs;4OqgE+F?HOuStr$g%5{eo{L+u@V@2#a3t7Z|ic15kmj$Jj{ zP-<^V?OA(&=Xu}n&2|3Hm6M#DbKk%D>Q4AT2|+j-tw^GwtOEiwz8rBLuxV}P@jcV9 z`x!78Fz1Ytc3&DKLuV$VH@}|)5F7B|)c}KHre<;OZ$E#tQQ8;qsm=)=$(TS1No6cX z!$J;(Ik}|2fncx)vByG!$j8XEqYx_uJ|jrK{AU55+#%{$H-oUSaP+$QRczQG%Wc2> zbsp;5bM`9+QNZWyR+8GxQ??m)ZGL0G85wpKUj@iHyuH1BeCnF#i)Lkr8m62<-c2TF z!X@0otw+hI2f`xSnY!aGz*X?s@@s@B^t(L*c$iokrom0yuTN$``|AhRNrX(EgMcUs zqWf{|lQ1pdLYb)tg#EAJYaeikyI^T~dESENO6xea-y|Gnl>qb!X9o2J^tEI*ksGS_ z^UF4T@s@-8??HZdaGo`O>!^_wmgZeO555ON#Eh2yVL>V56OT4m8Vk}PKWE|q5I8_C zf+TBT9M@dHT_8jExaetf<<`@@PDKtll2CE9-q<^rwjl5a1r;dfYoD~=0Nu-kG-4zJ zqzr9iy_&Loi6f6eUn~^@_BBAj!OU=<`XRg+Lhell6NkLu=ih3cqCPDU2lPUYB$lmh zn>7CRqAGPQt_=vD-|-JG78g5kx(^X3BpQvjibis$dd-_u!s{sRKq~dK-oF0IX$4a3 zG7JHMqKJEe;6*6IN_Bk}UDB^XA<#MyFr9p~F;a?1lU)LpZ&mCycT1WJN_T8rr~gmT z6Qa>X2rJ~4g}@p~Xe!apQGCxMe!s*668|Y8^5n}Jb%2l_E>8AHLJ=nI$4Q9EVVh3G zN|Y959GkpGkiF?y5dw+)q7v;9De+=S@tK^%H4C{(KJ=C+Wn$P&F&$3wGe>R`(pZbA zHrWNgr@DIEH0A0e!v4?X>v0o>_3FHDP*-PjYq0n1u0>;Mji4HoOHV)l9=xvK82VG1 zlSZtYVU-vP%QldJsBaDj=nYsEOgjV4jJmBYm-Ks-30XhKIy)`ZH}=n9FvMNjq%lVc zf#sa+S-Vw;KIE&*IDbQ~bOt*MM^^;#>Da%7&@(oav(!VFu~0f_F|Ijc`2nOdvjW9> zAEv=zs60L}jRS(k;|?%xp^}mR&GR^EDa}ID!~e3Xz_(Rl*Q^M+_7Gw}Vl3)L{HNxrGf9)pBi09PIqCK#g#L;O^NZ3_v* zB4bEsH_!2)?I%oJe;k48<@WQ%5O~N={KUXEK$1y%RtHW3rtUtVY6id;s%|c#Yz^61SoIGXzYo>#c*^~hHh0s*=By$x{Cmu&g}BZl^p@&K4#Bv9Ph>4*^~?NDMys{&6bpC7MWq8CdUpi6Xb0a{(CKX%jmP={P3gubP5;ZIPxk)OrUK1Y z6Y@U$6CIjv@r#$C%AQMZRdn&31oR>A@}d1cv_|>-UBic4#$J1x7!r}y7iObBCG*2H zUg~ivF{yIr?I-04^aU4ftoHhiZ0Lo|{6{R9)jTaOGQ5)B7rK#no9j-)dPZB8s-_|D z`4EanyMB~?(fEb6CoW85z5f;$R#KyBWYwS+nLFgP*ejn)wcWre-jhLbpGI<1vLLv7=Mm~$o0@c%Re%7#(-r3Ct>VP0`L{wmMhdn_;l~(eV3i!VAtHdbE*UL+$1F?XsWP_YBag85vS17iR^2E zCN~Y|#Ux+;@na&-^J;gxOOYJ-Khkr#d>*j}L5z2b z!Ql5fi*3HKkia)su*2#<8pH@6jPy}4GH5<(DmQF@ju-T&d(Ue2CG7jFY%C2bmLXPL{}y~E}?F9 z$$bt)O$sHKp%Ju-ONF!Aqoh7caJ}S%z~@Si&?Y;rD*Vj4cJ`mNpV2>rB#*EjkudIY`H$Qq9&ZRcK&xFL4Rp&D;5TzV zj=fEc+T*(-M*qV`{-@A9Cnp4bN*WSbdEqT+NEgY^A=jjEvs3Av$7J08aHo!v9zYGs zIG7o^g;DDPDB`Caui09($U^$AC9nA>> zKtK{pxx6gH`vqvrWB=?THt*66ci{M$l|)AW+j?`a&e<6^IHz~bSLJL>SZ7hh3Bb{tayb>&jvm6ED)bFle-x@kB-jxB4PxfZU$Q&9&;{5DZZ_0{Ez7FatkovL|mBR z&vb|$zs6y_EAuHqxQ_7dCHp3uRl8rgWriUpC8>QKxE-y)?$n@#VK!YDOeczim7N?! z6>E=>7y~IJ4Ij(&Z8p;#^j-qjQZdZ~NFNahPmw7SqvHPKp^VqyHr;@zzn?paTqLbO zL^3)0vjfv}>Ku;(>8L;+2(AX*6&Ruk+qB(ABkIknlvjXh2sBjB3XS>-3P*pAR!yYh zEG{C$1cHD5Ar3oMg6)2F#>(xa*2ljkjM1DCTKk!OxOA-q=2sF~odw|Mi!+^>`n}f6 zLBJtA4*UBs>=!QW&^GQb1{2G$ycBk04(d2Rw^iQ| zZoegfUf!IxCm?Ts%3t$xMgoF@L(u-TQ&pKQiJ&3b>Js7c2Bs8o4mf^INS4kIy$~u7 zpww@&@2Eb85eY(q-fjkBY*#Vmz)tn#i8}xFxZ8Eww@P zs1(aGyE3C`o_BBj@z-mTId`>#kh=B==b4k0^_Q@i0uIk8@>y5oF_W}9UUkcA9YaAn zgvzV(n6*RD{d{djsR5qI>M0YN?*eHYri@eM<}%U}Dt?in7u$k%rc$gBgeYj;Q*pZq zfM2L}TS4o20&j-y>$4IX)2uSi64#%qZWg0ID5;Ew+;t@4bim~c5 zM8F^Z)a9043&)R9HT<6Hwa9_S^KS`EtATZYw`#*`eP6d1JX#lV$#Z?x;ZhEH!rwIkwi2N`I?X@ ze8M~|2Z?}lLX^RtgPk0uIHD$pSi|oG4po&-pvJIUvp9fg0ejTEMi%{D%12&p?FAEq zVwy=fSy)){6?CIT+pga=Ps`?xy%?!)AYmn$O$Pr6q3JGR=&FbGJ)=}S+M*y6(B}Jo5Iwa zhAV4wZ=X|YY2(NVVpobxH;o#HvJcs>hvjLK7A{*CxC*L5cAP-_z>+nAn-9D^1iA0c zm!VuTRmOk_@mGsLjT0W2T|QJ@G&jeTL(g=gU>Yb`EjzmtOGIQ0!;g%d>UbhSxjp)d z0LC5Bsi}DtYVJ!f_<+!Aa!u#|KXc_LJnp`nxer32V$$mqn=bh9KTA_$qi|y;pX$Q& zF~*e-&}cZkQBVdylQF91^Su_EXWdR<1^62c*pJsp5=HZMr8hfTNh88`XUi_?(bCi> zR+Q2WPmM@aV2*_kL^zX?9J*;NNYm50on(4#%5Rz$7Y#}On7&GR(|?rUbr$o()Qo%76i<1`5@`aRR=i_P>grD6&wC`v}MmaDN{80#j%G< zM}^~BQ+i)dMMXSn*<5EIOIRcvnDtpck_Q8hBd=dsZ}R`iJ_HJrv{kKj59zxGRG6kNu42&G~Z-!Gl8KN?+ zFi2T@ZX=S3r~zIn$j=Ke&QD+<$lI73?9iR{DY*+!>f( zUlu3dt*c5+tEg|?5%Jl zI@~P#X?Cn|aQi8fYm?YL*uy;Nv_*p*_yTZ7mvGai@afF>}jHNH8@x|$ns z5B3mbVN)U|zAFwP6ICfeGR!_gnE0)PPSFxSmusGRlAVaS2vMTxRFx8~v2ie%)+X$9 znm_P8dtjZ-rp5h_n}+fOJ%Y!|TCFajjwYce@~Nhkm`;AX0HcnZA`g-zu2SYjH&^T( z;y)9OnZeYVj0jtcJVd@9L$^ns-yagbHyk>NvsETANhN$9iw2uML(QpE_G*wq$rm!M zUkpb{m%lDz4lDkt)VxK_m}-2}4*mgak}#99cGu1iHKu8hVYsktVOh6$Q_Z|CyOoK= zSKhNEWJ7RVFrxBFGZ*)xExR_gJ~+$7J19ak2VDx7izZc>(W!pCW61=RAELoL}a*Bs+ z#uBp*M0z1OaT=phjX0hA8PYw!->K-Sqt(~B$shd`a|gOEvt^!<$xOzPD|zOn}1SGKNRacMBs{i0^`4cBCd>)?RA7WG=-kO5JQeNwGqt zgK0!>*_glN2R~=>p%00VGtgKgUs5nT5~sZP$7cwuR^!I6bzis2dx;gGA1Q$bG2^si zKhLAv+1vSsox~^@X(e4|cKRB3JgEu(P$wyI zi#%JG$nzR}lOY|NO&WvpF-`aS6<11n5E5CW;#)P68C$@xr`1}*=$-fMVCbtLw(^je z3x00B%L1&!0g|SK|vJ(VQek zo6WA;d;lF9YDE~9YE+>Q1pKSwT{_Cj$i2o_fYYNhr-jyLT_$8alIETf>Ahd%{i<8C z1n<8oT|Q<%O}GY|{F~{Iv1fW-eTMj86C84p7$-G7RNnpAxQG37 z>6I$fi>0VHWbwWHW-VSrSUNc#PK<_)oh@U^cf{A!HA{1_btXlAL1uC(;g2GW5Cfq; zIOxoOORH?jQF#cJGlF8sIMC^7lx1$#Jio9--0pYRAOxU$B%nU>Pq_7dQu<(aJTac+ zXc}3DH*b(DTCaRCQyc|%HCZkU3uUy#Hx5mIF@)oJPieLU@#(?pXX@;Q?mkl#g%rFT zWw~Iox&P#KM5gOQ@|EB;ar2tn636)O;2Q-p!j{3Op0O2<1^N+-oDzHlned*U6 z$<0FLtAN`&(a*dE^(eRn)DOs0|^l!QD4U{NzV+cv$ z@T`L66{KtJ@_zi>b|3S({C2B$a1R*-PxTjf1TzNgk>0)8oSnS#1vyTn94XY0t9l0q zH_hOump!0){x>-qR|HC^JUgl;eef!b7dKXO>EZ|*Z zp*8U?fWvJyl?8M5jDMzF;W5@pbA_@3fz)O`lG|Wu__YiNr z`HRI@ICLZ%uj918;4{t!u>UF?-`1Q~*3{ch1q?~`WO!9vKd!n&_g0`QD>wg@=V%$` zOisG=`U*Z*%hI>8BJCIJgOFWOTq)e7uY`jThWRDq^H zJn=GHEf*}v`)CS(mu|Hiaw%~-DM)=ftKHgk-cwJ#4motVb zd&!hhvni5c)aD<_3B$NkL6Kf?685~GFY=g$UdK2vazl1`U|MX))Lx#a-<=Wq^q3yg zEX3zh>ij(^#L41;QpLRb+1G01W=YRE@A~qdJ*Z-=-xu-dZnF42(?NRcCVa#9cV56MO|kk;A!ldc zK3#PP!2EGTnu;>)W=$i3z}No_1T1+h5}Gs_;e{#I@YWk3I}^iF>q~z!LkP4qb}iVyJUW+$5R4Q{|?g4`{RKCE_2Oj`onWGBeQnTx={%fJ3FhH zEKscDrtp=je)+Cv8WYYB11aL`2w93kpSse6EXFe;E7c^wpVW7V^!aUKdr}d&pl5 zrrknh>PgbTd717s=U^hAViK4(v|tA>YkZCw`NFmfne0^+NCIlO2-yVd9GU-h*(`SR zkCo!`g_p&ItYsB(ol$GQ)c!hj%eyYU4Nwe*%2TX_$Sjjy9fI0QqOy0~T-;LX!((Q! zu`J^^H#b0Q&6N4gU-SQ#-=qI5|5Yvj)Wh2vYJB|B)dxqFrIcZ<&`h~%)1RpNMQ`v( zVEzX&myzBddMVvFU7O%puQZL>dekV6D7KoAc&NITzbwst_d&s^>KXOrYcsMK6%Jx0 ze;HqTkw{X*juFcz0g<5}ih4$;@8xu~GYD$)eHlX$lkOc}#E_JB?rm}3EFb&U<1DLCZ)hfZ!OH!doZU|!Dro(#L57L-1xNB|w~RLZ^?8L^ky z*@X;vL*!l?PpAfIX2pw&ak?*Ol%K#N5?sx(L+N=uPnk{eASn_#+b8_cQV!WDDasxr z<+ClSJEfNPxbAptR>0?8aso<=!~XkRJWG9dh-b(Zf>8zX;HJ@_*?Y-qz@3OK<&&90Dm|YRQQatq3%80% zzY?evsqH78?(upo>S=7k+lqddOvRpsh*zd*h+0jVwZgauiM;FH zGrhi+B{7;&so78do=N!Z?fCVL4Ss(Yk7Vc(fs7iV?|)0YV*LQUYk`Y!#=op)>cOY2 z(A*xWv?j`mtgdI~E-pf2rZiM^7rmGMOyNH1ur$gGlfC+DEEn7U++B8hkrI6}4K(!>r<#TO%md=qsT~AlE*w$DXV5 zcj)?49UZiyj;M@JQr6{(KeO};{yBF?n@u0xN0c38P;9wjPFlj0vt;V045x|^zugCt zYNID1>~rldBiZEv;a~1@M@Pba<>|B(4-miuAf3<+|U0#&S+?8^nyv4_GS?n6isTQaAkJT zyJOQwPL_f*2x%vVBJ@(y2`%II_d&1N#K>ish+|aOA>A&&k4gymi#NY<9eU5mTL!d$ z|3;Wxa3s^ z6xZ$tQBsLjuKJXJ8sTpD3pe;ZCy(rD%HrT2gZ$Q22qHlBGySe5cd0HGg8zS)6RsOllQ%zJJdksxzNztSC_?7a-%Br;ULIfn0 zU4Q%kbbAXO*a#Iq?0>BDFIduP&Pm^AR(|Fax|S57IS9+u%N%z0v*VP0lzx~PF?r+! zDo!2f-+b~DgH6#DyE7XB286LOOKKUqUp{1KOey0%J_SL7IHj>zozKmbE|!R5gr9i@ zU_KD&UgW_BS-7E4g8SbiKxmc7ffA4ewF1BBXBE0K2c}aXxNr`A zZ!?S^3T*Nz$>N8GvRP$sP!!ydsOQf$Y(%YJS@Z0Xb71`_p{g)J!Db`mL zE|sNO@e!x14vfDqqEAmxS=GCgw}1rD+3VAr#fKodmQx+>Db)Gj^Z{Iv1JI*OV*TY7KLLC2!Xv z={!>)Y}Zv~8Hve&t8c)#vq4q_(srva!*vk8iXxIsFA%nyIFOhf=#C9yPz#2_!knQ5 zpgP@HStczk7Rz6Ju87BylP%pztF=CcmuiM5NH;YnwUw_(6`}wKoEgCDB6Lc8V17Oq z+44WN=a^%3>Te%gCX>z>hSew^;@58KHrXj8!gR4E4-i6T#H47dIqj)@s>V>GzIP9g%eYb2k-833X0gq41C#(#pdN!I*b~}b3t*? zQAC@mp2c=00w*34e`5Q4Su7gc1Wl5DS!jw+ROKGtAHu+jpQZKWjY=RL0g1MM;@=Vv z18ULo&&mUnvMN{0a9KEUfvs%iBD?fgVM(I8=c6Jn39dKcD7L!r44w;IA~48{oo zg!d!rF_Vng<}qF5XVhN~U7F$k{u+`;Yj?qhLTlZa#Z3U4Al4kYx~RGhoT-;TDwI#w z4cw{IM!wt`$N3))NX=c3%eozhgau!{h6ClLtdL9Kb~{vCwk3IZZdGHioC$y{qQ&&N z?flu;%9D`u{aqIj@Ui8kAN_B;FSUBvOg-ucMxHFj@SY)|{cJyIaW>U7{o;E2>fODaS&_t)CJ>DI5rpfT9W zj&{zcSuUCJZ%9&wT z)7pv4m>L7U{eGRCDVmHVD`>0Ep?j*84qK7^QdlF z6kbTLQTf=hfqoz)^Xd%TW*aX%cc{oB96$N14gQ2K{0L43y23XujVh987eCe)>pF6& zW!7#4k=Ukf_Ha8gk-p}UEY}>V)-%|8@+-(#dxktVOI|28O;NZHf%2ZX9()*^= zSBh;D6Cj}7-stW)61J5lKvGYw!ud=K)}3z>`8wO*Kc7KRghGTTCQ`>t5MPAkRK$v+ z)P&b5lc%VM>o1c^xA?jf1xwC`Dnzr#n4Dl59eSQNs6BG8IBBQE>uWYthfjo><_%x* z3R%Hp;(|v>TQ$nRE`LiZFoV{g4A2t4e9>v6Mj)O1&L<766+=Ids1VFGyVI?cOJbOpmP1hgv!McTClPSGv;KCf_cc4 z>EV~86V0-k%A(YXa-lUrHI=vurA4mAW?wW~b0o4_+x!CPEIrK{;7LV7Xqw|aLDA{DWNqrD%0z-^b&uz^9dXJp# z!@W!P{ithAWQn-O_Zc%uPQ(+weoFLBVu`I zGO|qwj-Cx)Z>V8QYA_U-brGSG`;JcUr(DZ7x*OiV)8y51RxDb9u~72r;NdB(z}OT5 z6yBNM-yK|)Bhsds_&+Vc=7Z++RE9+(0RS@i)%}bUvi>qFTU~GX{hN!^{I3$RQ;#20 z;oJ=piQm1b8&Z{BP5O%{L~e=qcX!|C9TV5w33S%>C|n?lyibSCi@fjK!j znzRKD{}Se@bK#~NqI7ejU-?P~7rDwr)pK`a%S!PEpVS6dL|j=gm^V3%h@)#*Ddvel zbzN`Ovb5@FHypgJ1&V+2ze{EgH`=nQL1G3`P7=>|f?n_^7{KN}PgduQp<{C`xYoWp ztQFIq8pWtNzJ1_aD240($rAJ>wl6TGxg9I6hsn$WPQ-)1*dgj(8g&^N<}+Q?xcc(Z z$LPKm4X82yQ}B+EA6#O!zvz;elQj)k15P)+!VcZ8uzBaB z1Gn3IH-7+8{IxoC!BB{TI<9Q|hX&g5$4<*N zW!MsZ`yTZ6ci4Wz%}Leubk&3BIpG<%xAizFr+vq|Kh-$e8_dFGNZw_1`=lT5?e*<` zx%o|ddu_%}N$PC2H0IdD-0yg^F6`Pa?8Jii_P6l$Z>(D@)F~a0=5{$*JC~90bK?fOxKUItxm3}N}Zb-RX{Yyo|3;?)@!e70JCazCS1~&yw_^c)TAP1GH;cy&( z%5<+`{C_uK%;6r}E(RLQ#{tP!vO+9V6IoFWxk$m^u^u43J9hxpYP87HV&>y71$~XO zAr}m}^BWVFdwKyX#0pB4QT1{@ZGL#7%iG&=L~ocNMK!k74Gq1x|Bxqje(oaI`Ie2! zUr)nw?DtdKPWN-sZKt{28^mwT;B|pW-lEjms8NCN5)>4wn$h%lHUu-3VMsBBg6YY4 zVv$|Zkn2}Y6{a43k0`*5qqJVGkV|ezC$*_idJQNGRU40b){Vz*w9NQj5~Ml>wE1Kh zhJ!G*R6m-0z8aM~>^POxy)QXuzge1ib)0kRMi3kvoUMz=*rQ4U(yRf9bJ)kUbPR1E z;n(bS={i%NwD2%TnUh-_GMnOv%Ka7fg^$$7zN|G6h4gHJeZrSgOPn|AYrtxbIxvcNpo5LCm!~|4Gx`RkSvDI<0AO8dq zJd{}?`Y=f^cT))W#8_xHWaJd6s@GC3T3`S_O80^|Wfa7phOrk{ z{SZFWBk^R_&tivGqQr*VwoN72ehD~dc4zhobEJ!pHl*YM=BKms4K~#OHkYLJCOScQE$FaUHYwhyY67hc z3%z8&<^l%<2a+x7EnJt*UH8<{gE~H^2JY=fwC%N@)ET2M#v0sEhP2jfc`V@JxhP7{ z!K&7O9w37%GndTO3cq<#%#?G@lyloHR35ZA61EyNcips>%T=&&v$KF$rP5*icZpf5 zL#7&mY6r4Wjyt_VOM2tZvURoBYwQwzEJ{)HxqnNdJ)D-PAmZ_jmxxxe2t&>Pl#Euw z#Z>vJ_)Mwqh_+4JNzqZBwz&>uhRf;{)$x zI94PlQ)fua*c`2#2M^+`91-_;k_|_wbw2-v3GFQ|qOtA!vxlI;@rVD3!Vr>ycPSnO z!+qXhnadnq(xFy)!mES+!M;XL?8}d{K#3)&zq5TJdFi_M*yRY_t7logV{nkCa}`p% zS~02AD4HOM;(bEQ{lDifHvFFw9M+r-kf_-J^G$f)NvSxoie zpR_1C{Xq&aK&6%pS;ROmke!wPG zIr5L;hdI?qC5uD{j`Yt0*|T%;9XdpO#k79qlJu*(mqX=*?w+3b@ieh0Kwn`vlNQSwjY=of%3sL*`hoxL8M!`6+LUtUVD zx!ra%Z)XTk4skC15mAnNlJzM3 zndjlkSse01dMWH^aVa0&cJ?P0_qCy+6n(lY|7r|SG|@rX+4C|CPV3+duXEW^vnztV_<&SXn5Vd#m-ycH{S$zYCW$3 z6seaBp@;05Q}lon%XNlMu-iF&Jce6(ZDd3b5qdi3=TzZ>bw4$3aQ&OTH{WXP^zli; zJOtuc=ld5>q!A4ko3sXgM7^7w^Yf$Iy{IuTt7#56_)ZVMi<+nmTdOeMoC8W4FCAG? zXnG8pzpHV{?1ay(Oxo|cwoA)}Q+DjR=Y-1%B~W$KY9^%#%^E>bY|wE*va72^KHg zAj-v$xa`&o*PbkMv&C)yf2JikB`RU|r$Y_3RMbgp57~q`M)P~Dl=(T1@@8lt;-)ur zCF*u9Y`>i?&o#qy@s#9X=(OlomOnUog~_#@Yq++d;dDbg>=)p-_qjZ|9MKFV@h(lJ zJ5)x7NHl@LkM3wQIfg!bE}K_%nVg}halAS}uoRJZ)!%-#v_r}ER7UH`DW2$veTDRz zQE*t;?R4j?C|^z{y6DKqyN>)eJC2qB8pnEXy>H?{R9cs&dJ_d#PIr>ATJ=xQN#F#e z$h7#C&FUM0u$LWj61}fOYH{ofUs*persa9^q1d#na<2wTID#-u*h>Nq{(t)3`E%8O)!dh=xqdw^S>@8JTrMX5Bn|ypq$tVp zZsu9`w6L?l5JgHcVULZWLPRMSV?z9MT#0$#8eNrf*gr{Kf93@FuW+6R%}*=@Byi;2 zI8SxFIacKw759V20f(O>Y+U{KH6KB;22|=qRQcLS{DkYQ-#4i<{quukQ%S?A%1%}W z$#w4%I;QACY6HXr7M z37GS$k?3GAebwN(3Dv!6rM?q4<^X@haDaX7=gwLo@(8Q_qUfhNdQusTL-rpJo{0ob zFNS}WhzXeePSg5u^zL7^Dq80X;csKi1}POE{471#?cdF?PZ|}eHe4x})c=XbTGa0R z@?Nqw-a=I0iAQ#f*bRn+M%|DuvcU6;BZfBFtw!ruu;rgHCW8J|3ZEfP3`uukoXO%n`ucGY{LgVP09OA1W|E$@6R2^Qghl~<^{a^r)`K5NfC^c; zO2Xhwp*&SVZy!|kKWZ92XFaYhlgIO6^2JpC+Ajg-MC}1}orh+ZXXWx2N(&9neWuY( z;{w!vO`o|FpExPkM(AhgLf$boQqM>W1@BAcdqupp%mXpkfa9{=m)b2DzQuo9;dEk> zl1}}~McWd^T-3HV_282?p?rJfipR&2cmow%i&_J`oLi|j`<0KA$mKLA8E!6OY1L(8 z^D0K(hv|0@j;5YY34Kd)&by|WIll;MiIW!*VDO=3v{2EU7` zc(K5NI+pz{Lh~0#9boPmf7HAGggOB=GP0Oq!Spg6M3mj4y?aORW6;oSdt&v&;=EsL zGSjH;k>#U)Rexn5n4}3ZD?YF|K?tJa94jzVoKvskvGm)!)Eu00;RAt&n1s6j>`JZ% zAo;lIoM{~=wNkV53O|UkYG@F$GeQ25>inkUr#|iMp^FfwkSOE)*X=T>8-pL{?Je#c z|2UVT%6!Id)i6uBC{3u?JNS%BP3uE~uDWpB83YGtw&eP8rM-5zE7|r-TVjCIxX3Wj z#5AEEWlDqV0*E+%(Ie=*A9(|hB)^MR&Gt+o* zg~-+|D?*@7csPWn4H-`3vTa-@Pk8pJ#zcZaFWRi-B&0Ra{n6SL5JRDr+W))g9g5=itO*~!w{b~$`Ss!_7 zf*dp7qbTp)L-I+yAZb6??2R33SO_hxI$rt~rS?8pIRx80YQ;>>5 zaamW%C}8LuGh&x}jN92B+zA5y)2%u@WbzH~`*B~9(DD ze}+6>9f(6o*|B;_hX$*|jN*_dey100?adACWy;e{cdPW7-x5lW&v^}&mXE032Q+7c ztZK<Fma} zpS;Lvs9tQ^=U{KsDA?H3foULi2ne*TH04TO#?*Cs$$FhmnBP7Q2hvn&z#d{4r-{n) zO=h=X=J1RsZmq|an5c%KK`Z)><8mqdyI;^cfm=?!xP8u%Zta37E23PhAHSRBnQ3U2 z>D{T2YTWCPq&jvRcm(r*YD}k*!MPASw=$y^D0G>7ciqvW0W%=u>5gtTH1IP|5iLYL z67v|0rlD8DKMg_JKRD@h`P>XXUm6Zk=n7arI%4Q5+l%f~AOphOxV20t?qHD|*Jy%` zcL#@j2fC+_FW9xo-t+ljmL&oe7e;oK z^^{AULV%$ID^W>(@;!zx=_%u1C^>v2uujECzQR=LsEIFNAa9_T&)o75p`+~ezsS72 z(3LI>Ilg7uAml8N745;srIOV{`p;4@@{M((txJjzvw=~zvzoh~XYm-W9@X)&TC){> zCU$&IfWn3xRsqAy%F5d3xu$P9Yhacfb#&dbw%rDU9_8i$x*;MTl1oy2&K_U_8X*$(SsyKoxF%I-m-pHh?yEjZ~H+hw4T*JXRY$tYG?ztL2*3)H@L7hW?} z@nb>!bbF%e3|->0SBiGnY&;oWd0jhhR$G2@d|co7@853IRpHN<;F*~zP4%=#CY*cA^+1nNi|XJ?7}kdTmos{Oy3JIcsPd!4nk zw50s)*|Y-aM>EAlxixybg`p%f2h;}RSU>5^zgwA+%p4n=kV``~xnv{e=jMoZ!Whil z&gQalalco)O}srz!>_YSUB+h*ZaV}4rez}8CpMz@{6b8}fNB+jw#oyrO!y-gc@hcl$H=c5dOKP2ywf z0}h?HJsXm9g6jZ3Z+iAahuR)O@$@9if!PnN`#VB`cT}=IwNu}8%Tfgh{w!CTYFzL7 zyDs=)oxz+mOHN+kdM@cQCEFBQ8zD1QaSasC(z#wc(9dsM>SetPVdzltk{m1-nqqf3 z+Ns@cE32x@`$Uhbhxq>I5R2RHf<{CSvYOAGKdeVfnM|U_In~ zyJ1rFVaV@bCk=hD8MO!dwwCxk;27>JsqiV9|Hx@1TSB^KWcSU{EN|IC`RJv|`F^OBqc6NIK%8RfEqM2KQ=npO#$^ z4MMxjuYTNS2QLL2ae`dnYP*yV*3}?3xHKf=&uXt{OYuDN3qpXyYMKt&!5>UD;z=0H zezg%hou^@1;w$yVLcAlmv$_KbZ_j#D`4YvEuK)J-_I7vYn|y?Hp5q-vQ~fH8i{E;w z>-_2l1hKk4QvUPjT^ud1HJU)3HBY(0YPb)b80dSo{-FQ zaWw@uaU4PWZ~#Ip`S;^xP$*pYU3?dx9&K zPd9wm`{CD*<>VMQeV!Tn`x!CRbxHT(uLxW^3sqD8h6-Rmqwze1Y+gbkrC`_>CIXEV zp1*wMfsq44xVT45di}9XmSMfVV+I}Iu6vVUqp@<%)Tjb5U8fEb`BYC2*eMLq=Y?oW zDB@~otJFEjm6aW8jlO&)Mk_Y){J_A$E|~JVf6XbCDKrZE=t`GwQ>F2T4=T9lS^qFl z#8gtBeF>CgFXJZ4EK`n%+-9`pK6dwk zsVW0{8??n?2!=B(;?N{S(89erKpG9=+!}`B-w)n{ z*cKm^<8nCt{!B`J@c5>=+UYDo@M{jh>fl5T=IBl-7n3$W`NOxn@;Ton2do*O2PQFr zLhFq*_o>Z^6^8B7GXT&c{joVm4S|sKH-C69n{73oP&^bTSw1<95z&}q$x+rAC(IXh zlDocV`2;Xeto*#ky`y29nQOFrL=rF}4W%29S*IURS^AJ#_bMU@%>FQR7Gh+qV~Gcp z9+mn1UQ{hGp@P;EbdT_-r&D{w?OXCM^{P{F;a^NAMMxzr5{2!U1}`*}+#CKjQhd&_ zV(fd$00x)SPEf&K=Ts;_9TFn`YqJl6=*#3+(xT3SP8X}!>ZcJpjLsH(DEc!?5SbLh z^MPWOKnG42e-8{BnJN!0(NW0}8V_8c=2m&~MiY{16i(m&R8l8Sa$r^AeW=K*fo~2U zx$1d&lHGDbq}>Hygb3q`4{?yCrEBga?7no9zDEM{hXOMV;g1VXnVPsP+i_IAo(hBY zvXLVT?K<|1$rxH{*VDR}$*){GQ?8IEG0CjkMw8eo1nT^y@nMBDp3)dD(I8glFyXa? zq>Z5l_bqpp7A4Vs=yAxb_qL53;_{xT>n9dyw9hd4?wIkXTlHU&GRz++BBVmjMMT>G za@6KVudmZ}hB4m%=FX_AR>(!X_n(|l#fh-vQ|k>y-1h%ZEw$!C#-*EHNW?A)7wGX^ zz2#)W!}gt_5CLa*vjc12m#=#bw9N23=$a(%ul+$dw8lFLz~}WFa36Z^0Zup!^S^{B z1M@O9Yw4^CC7Z7W3Pn1*${)Rwe90kq{9)l_8gbX>db@si%zSrle!F9Salm?&Xns|6 zcRp9>m71OR&-=!3K!M9eA!X6;=1<2LR)2S&*5^Uj=Y`U&vO(`gUD24c(=)1|e-=(lGRn6C(ss`Rv ztTLBd6E4|VvMM^?^_EbpJ3OYYk~#jvhUIc)+auFwRV~N8KCQR^`t;{pPf}<0+G5oW ztKx>O(^j+eD*78n$R0egCIE2oum|ccQ*`U4kIo{$AiSRYx$Q;g25tQ{ZEN3hDs%7nrM z!gNRKoF~QsdIM^K5m# zC1~ygJd``r#`AAZwpWVx>4QE;$;zFb0Syh7g|=Yx%T}lHOxoNvMTOn_e!}or9eW+W zi&SiGThNMK;Jf1uH^g4O_ZPxZm*md^d0rj0b927Ipm5kf|4{>T|FuiRgq8#@JfqH@ zcMheSsk5D8lG>xwOG&zfTli&1|6az8-MB?q?Or5OG`caTf=?S;3{^|j1mWQC#_Q@7qv8trkgijKG zS}gJQG*d{o^vlT5@rSO)S!}<~d0d$LdV@D&j3i{Av)=b;eSUuaWE*F`zd6C-aY56g zC2}{LCw@1>uUWIRvT(IE|3NUfPyBkp8YN(xT@1fR|7!x7;l%T`)_ix!7XwDe)Qc+CwTN$3m=H?-jL{eDPi z*1|tnliX|aA6Q=PWIOau!=ary;%F6u=AQW_o|#v_N*aS>jbw?DjJ<_ z3vTPFxx(f0RhxP7isP(!c#z=z4*$egVqjrNdCIfio0+MpDF6kR<$HAHp#Hk}BcWM` ztT?p6zd0C`+@TOC0xW}pjR!K=TQ)v~-#XN?gXWz8&t-#BXe1*`oe}G+p8MZEy+jbS z4-9Bj)NViX$^Ic=p*|vCF}}50-9)l(sFC3LwW%sDJ3DzxD&XDfuIa=*D-l;@9vD1Q z86Txh^c+!I3~oZ{5Qc zv%JK$cU)}a2Cls;BgcA%a{X{BYjpiQs-6?zh$(+7$Ld&>>gHXh#B5 zZVjq#dh_Z-Ju~F~@ccX;*j3^NZff3o5v6o{gG)+C`5Sz+a7zY`wgz2ZyWZ}ZPRunN zy{I_ZyVAAeDl`tpeJ&SgEww|*WY3$a4Jw{AQ4|GY(0qzQhJb>H|o zSCx~*eN<)2ut7KTO+O(e4tqB@+SnYQCL3~Pi{IrZr+@t#_f7+`<7K+FUvS^Nx*1?= z406?g{z-m2A{MxbrF6%rs;Yu436-s-9ue?JiPEY&q>YdN0a1KLC^OrS@&kRx>I1I? z;Zi}@bRj#_txfhB4u8`PaTQC(W`^Q5A4sUsAxty}@BT_LSjJ#jCYhe$E)z_8_bHPI z>S=nJ%8y&`{PuxHZq<$EGea?ukJoxgZ?83ox5`5)Tpj@WE7b)hw4S#6yD`SRwcMG7 zf_YWosJ&4ynX;!~grL+q<8O?v|}RT9uW_f#!CzG?kUJ->R*xrS4oaFj;CbXR1JNEnnk<7uIGAOB4%vMUO0m6H;Qb70`+>af&YA+=QY)oV#no zgy*sw^X1FSy_?#JwiIN}yXAXe<~Tkap&lGO&rTp8O&8-8qb&w+ya(!?`t?A)avMPb zucsCGFyqmQ_%rDX#UN>4zW9ZN5cOf43ZBuQM~e906RlC(oDv|!n52C0R)u8mK!*Re zAQDZ#YOVKnmFX{ky15Q_p8UU4-tb_LDv~W9i}AK`u#3&Gm2}}uJ*x5@k*ud?IX`cS zmkrpzPbIfMZm`pIo5oBTe5tJmf=Bk7ET%ir)$Rziu0D~x>6W|7&vdDe(Z+j}9ukAg zvz5F07JM+WurILpZpJ5Yrhp`&cJFRD_~b@Tf~uI!HfRf{51OB!_`3(q#NTEHUxhch zTxiN&%beYIT+ui&jrT;o&>b*GjET#g7Rm)|tK(9?bkHqFY?p~tZD-ivU2_(Q2fxJA z=Ttl8CILs1!FyBzcMCUXcUyOXr|nj9r-OIDVFoRw`!70T+@G8H4i4U|y53#hNtQd{ z$7M8AS@g#A(8;&;e$paHFd52VqZWC{A;wq2dfgf5@6zmiCjMjgD8qL5ro9DQ1h@}R z)vmTD`tmvosi3wfrg&KGb2i|L^O4NE%~gKP1NxC$>=$+AocEhwu@)k*W7amJgbecA zA;qs2$p9HpwGBT-SFh&ry|cC@^@a1TroE@we3c`MEA@CZ)%AMKw`w|&bh5;XSe4s&&J&a#mz?f&%-YonXCLqWU89C{+h~g$ulz3 z5!zD8-L)=UR=j2jel`62Pw6A4`Rc2P)-(UQj;Y++vf!h`iRRNW^ScvOf?j(2Qn$;= zrL$(NOv&KbPwiB`Pej+VCVQD}7ZpNHCw_w#_*($LB-<0bVh7QSMgN7X%_|lph0!

    #F)VFuN0(bF(S9h0q|Cpo6u@Q3>n^Z0 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

    -W(rzm*Obq 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∾aOPd9a5jBr!}f#0)G;pfg++x$>QWjfk4vMJbuS+e?m8-9a2*-|R2 zK$BtwA}Ut1^kEjNJuhP8!S?2|FKN80j(qLpkH)k#*<chJ-o@F$iLF)} zl!h<>ld3aOXn`ikN!SDOpUK^agIFGQg8K89xnUTNFuEZ=BA!fV+Ii>U{5&c_DE`G0 zq6(XGvR$y~dNz}+U~t9pr`2$@Ec9_;q>ZcNhr1U-B4B!9=%aTr6;E^3#wH&m5qpQrvrMc>9))W`5}4veuz_u*_2_OeDu-KW;i?tD< z0jAXHNGwrkC%eE8y>MQwj2J?7%y#iQFNWiSyXu{~_n{CwFSWg@DiOc4GcR*R6w~wQ z0qox2Is_z46RJCEDD&L!9|0i~B=B>T8k89#v_bhL&Apvy@fe*x)!%1=_0(H4TQc1R zM}dfgHP-v-c+xM^^q|aFW*;NvxbnJHa!2KG_Ey!?sbH|5`j(>5k_=gWvpwi;##?k` zf5$YKl`vvg<{?kuIpq1$2_{NSyNbT`o!(_3z^8okck z9anW1zn6^L&ka3Q}>OPNgt|OnynEvhInbPJ{5pKEcREOlrwJAe>{#1F}yC zqD5G)K0Zw_WZLidzZ|1oOZCyoY7Mz}1lYs&vHk67g4zt}hyT46=jgCWOK1Lk`0GEl z;rzeCJPzwnc!>OM(?vU{oneHFdY}Q{m1{lK$gW0-D&_*M;m&rPgB1|_mBozunP9j_ zu&zNlhsc|6_B!^Vd2_^X%8Htov1Mf)mOrFNPA`RLit%b(8F`djlPlFS8@konqy#?- zk^82hLzGvHTo2c(z};C|s+v1R>U_Yhdu^LUKA5GFjQuSg&gzDc?oOjX1F$A?@&%&y z9TOXcyJCBPsqyX4P+Ht~KNekXuBMV~} zH|wOp&r%dYe+|N=N;4Q_{D78lIuHNr3nzz@|A5Y8vb>|`Ted?JHGvaO!RhFIXxQP= zngiiWN|Mr<+P9abM&U{qDdj1OdC?>jO&%bf5l^ zol65^0>IRQ!<*F~&zJDH$TyeHvG`gT;UUHba!h3LlV z+3qv&dlg4f2vELJ{KsLcUaal!pKVa7Hw}D@doKCPek7mceq6|U1{>u0m>^c6f?06k zz_#^grSC7KnKwddG9px#8<#Doc3Y3uGuACz4A(NVH&kK1x|(oZ;DYYwXNJ^EfFWi! zR|l9q`o#dYMi^T9tSTnFPv$cFcB9+uFm3VfE_rb6A2$L#T6z2DZfTFq`0o?z0jeA|bmGW2xBX;9yhf0Y2M7R?fN(+}j?I_ROFsJmyzLNF#frIJWcB9>69^*`UX`B4p3fwA6$!p z@jOgTG19d?%>n(;`xg281t{VvEK@51#SPK*&5A ziLLFOV#7@Oj1tz53jEhTwC|kzgkPK+^+Lra?sxlDi&@4GQ)gPe$x*(2gYtA zd|5FqI0w>UP8dD@Da2t~i;^)t&2t`{y5wM=5x!YK1wf>GV|ZQQysZbQXd&Kgg`M{z1U=1%6gFfDZT> zaoQ85^^k|qr9!B$d=Eqe27YIIxZoi27R&Gx@*#mt(gC#=hZbJ9!qf7&_4>8Mjq!Ai z^m>>!*XY#Xr^p*|Vme62)w|Uvkrs~*>v_+P2aY4I`d6%0D#b=Fh_gqGU3|(uj{L76 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%0DLbazA?X1W7g^1`OzN%icqB-af)JUnM@KTq<+C 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%0AF`^_n;tvLFBMo9K)_Lsr%4J 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'~l5EvCWQjBC zV8zk(iu2LN2TaY-ASR4DGpEWN z=sp6G$iJ^`q2x4w*(>l|sFkU6s`wY{7dRoXr)imLZ zerLuZOI>Ax0m%Jfnnr!3;4{d}UGf^Wgg&ysq&I?r@VFh7J+KV*L%6({rdas{+)85>znOA8NPXO1Fh6fWcKnHY+70{d2d>; zM}dCs*rq@!Tp8YwcLbl)8sf|-k*2iVS!qKdE`L3s^an3W!>8 zO`N3}B4Gy!oQFqBXMVdLr|uo6?$2{OcW`q-b#VJ}smPT1-JsHi&BD#NrihgG(GA;M zX^kVCyt~*;@XE6v?Kk+xWN>F)#onBt3hK}&2GHYZDDsnh^|-X_<2$exSPj`6ZxlnF zw%u?n(so{lkV+H<#aCu8G(`RJkOFo4Hb=jllK|t6l}Arzk;4KY?zH(8G(32MK5(X8 zS75}qjJrqF-7ujGM@HuZBqiz2|GVCajKl1HWt`e7+n7pjC0sr}Mzi2D70{Cnde-~I z6v|0^)KMdNyAASCIVGjdX!*e8_?_Lxa8q|LC-o9OnDuZ?HjKB!K2YFcc zoL9%LCp|s=r)QH>zrJf2wVh&bOAL{RxJbsNxy_q@PM~d9rUVjA^V&PM(?y}_Z#yj% zOn#&Fr;5oT+s6JzCejv;=^m-nPH9xMPiQs7(&+f!u>x3#8k7#{@N}Yc-YG=_Vh+=j z{~8ddun*wJ-+!(iK@iN4!Y`jH_#}->6X+mAE0C5pz?FYb>?=nI5=V-LTosGs}m({ zgf5Mgj3`IEs1%>*y-0$-8>!g_Y+V@vh)a{eo={HOLxW}23^8|o-%sr?z*g9-h<0PX zd(TYQuUDK#{d3Fga-xqUGN;@&pwVR!{qT43aWJyT|7_bq!HMJGC?g`e3)Nz`s^<-V3O7-tB62U!M>_Z(Pt8gdYLj2FM>%x_8 z*uz^7_S2i*`=GOhM>}(2&vz<4$+vXJgtzpPJmEcmb->-ChSlt#K%Ej}r-oE%aU{7U4|s*9&gUv1p4*|KBz-HyV(C*JJ{vICu_&dspjjV*%RJTVNwT zJr96g3?u!4S~~EQIoYNIb=+P)g@&z8vp~LSFue~S3t5s*S__iRvHN2LiaL@>s+`m< z!)C}lNNwr)Aa6~fpZL-AFASx-F~88{6BSoQ=8x#crY+#!zi~SO^yHeNHi2!=uEIuY zq`elI`*AlD)%9hjvjXqPFiuq+r&dW4w zfd9ka(kUjE%E5aL7pCT4fa?nsgNhNa8ZGGC{f`=8iB1rK3X2ZwM5oW@ux73;R863g zY%`rYneCdX{;G*hc=H|#m!N4&`+Kt&u4G$yHSau=cU>wHD&X9F&?96t=OJY^(~Efe z@4MrR9E;y?V-)% z($>>McS{_D+lu8&#xGNZx-oLz3FM$rZ~RcdnnLnOSfly`=3F3!#Kr7DAwhOvRw_Xf zzRz%Em)An@w~WKSHUof_j@#w(Z8z;C)qq;qayRGUf5cDh?P>xp&vS0i<#_5%k8iSVm^ExC@SBJMI) zc^(jr$~8)K>ru4CcJ^qkNB5^))3D@F&%Gtw2PZ)L7L`pW6( zz}6r1SNZ&`<@)R6@cBO*b(_~3g2^1YOW&?v79P)`ZrM%@W;R+Zv%^39;@rayl91Af zpX%A@f(b;rl>H6#9`Z%fgDr<)@O<+EcX4OKIHtGE9&NHU_21XXnt6WNg%iXLB!OO* zsVE!OsIqSl7)WtzMW+#SeFZj#MDY&{l4Q9;JO_R6sX`qXLIQ@;AN=}T*P~vh7x=^A zu!n*x0%7o?nq1uFeet@CBHaRgmdY-`8E*TiO~5Kf*s9lZR16?=3UtF$}H+eGHf1|K)$N*-&Tq zuwSO`n4NRb1FAJ3@Tok4vsC!qdQmY;**DfMR?WrPse`{SA&_<7U+)f3D59Ht$lN2l z=?=Dxrnpfri$e*Yl;@%672QNxv*97ln?X0CEdGPiC1&@Zq!erbvzUL!jj*i44%1@Q ziJUYJUtRQS=tFwBnlmBe@Q^_9DD|2tbLtwuG~q+eO~=5`lg;~`sAVa#zjr z_1wqZd1i}0iU=uC)XH_}l*yd{M&all?vLOCni=ujsf?U7^;Kgi-fQd0zL6Ymd+f73 z({s9@?|e;a*fp*`5Yfm+Vj~dZs)bhI;(zwZC6rfP#hhy)6CTZfpRWipO|6xhq<(M5 zkINv1=2Q??A6jJD1*CL{S z3Fz_D>n62D&>$0t#Y|OHzM#!kv^S##fcok$PJ%O=C=u@jXj;ra#x2Nbu>Ct4wr#$2 zD`!n1$@&s!!fWx7@zH&k;1AKFbS~uC;2$N3=K^2?&(Fbje5;8|&)LPnZ`u*f=IXz) z)r+jbo8zm?BP{?>%#ni&j{Zd0`}3UdfJUtqLNT&pTJk&6UA{)fu@vOBS+@5)uV2Y0v*ZH$+J+Zv3XOD zE;E_#xuD5Emucuya%F>=A(!B1;*@Q{0M0k}4ThVNqtBX^hQuXdW+iPRASs|xr4W;H zB|`4|OZ$QDCvyuJY!&p85{d~_ZG0nMnXNCLpyUQn&e)0A^pPp+Qs8wSw2}za981;- z8;GzZWg$4p!QT&pFV{;j*GvoKZf<@T%!qM=A~-~+?yGc|+6j5dGZdH-^JBqOxLc4c zz8-V8P6s^#&h6=k?ahQ)&asHx8DY(AKfqVp1hT-unN?9j8Qfog^d0;YlvAdeFIs9V z&hsw|Qarn@=qa%EF)p&I4Dq$~q7&3dsW}o7GO4yB98?zhe$hM!zc~apxxhsp{`I)L zezbxvkrM)bre_y}$`=3YAzbgVoZXP%W!7$GRP82k-z{Dy?X>l=e33Ai>>FbJr~Zqc zO6pV{9_}w)kxyFe*|WhJ@y`ROBlI8wy(x}qCHpPNQaG$dfk@M7ae9;u2*x!*Tr+{*+O~!^p@;q9rgGu{ie5K zu*XFLEchQ_SiV>g?*@^M$P&lE9h$x7ZBky(!Y2V~0}Nz|Jx;_u9d113A3nK2)=R(B zao-x@YEXo^7W9pcdK(OZa<+fs01;5V5&vfThbqoLHg{eaA71j{(sp#aqP!%NT6YUR z^U#8E6l@=(FX6&JoZ&B1#5$+RT+@5rw=wdkb*WXbE!6CRM}U`kUyz{K1B(Rb6J+8}(ms!KC|Umz=* z;?;+?5r*zU{*Gz}-Nb_gv8e)|zL^9>%w9zF|2#EZ&8j+J{n$i}xHKGo)0Q+r0I2T$ z6Dk#X?~3_-tmFqmvmyb4<*iz8aTMn(?;j+q4<(CwB(Xa-c34|8-;c*T=8}LEp zR2=_ma%`cwq6^V`=pzWad6;A#I+0=P5rnV#7;pq-`;6#a!D?wl%SdjIcC+v3Gx==z z2x3R`aTX$x@QeFK)d=)R=K7(pu0-qMp?g~SaAl3INc*|{OzBzc_5$Xl??5{+{GwhO z?U64J#hNtlg>Vq7{Y?jh=?JAkHGrQ!BBMoW_VdFAjWVuL8MH+wEha`n>E0yu z1K26D z`duaQ8Jczb`_EHOS=KkGEafhDYtY# zDb5HT5nY(9C0AdysEtxrY`XG42L<~J&j^}8$No0 z%Xvv6aSRq4|Faq!&=-2^b90ERk`|a248w`g^oMR@ouN`$sj%dvq^b*ozqlh0glbhl zAg=UpEL4|$zz+UCmk7Y!CuhU-mxB!|WNTF(ln3!}5`} zgld*sF`47yWXKP+gnLzUZ2A!08`Av|3u(05@x$2W;2D3M#xw2|;YJ<-Hi8`;oD%w7 zu4Wg+enlh<3?}mVZkbg2!#o=2$h*DoZlf5S9_RqRnQ@{m2z$wJn|5;HeCrn@SxQ0< z&aLw>7%3`bpvKbLI@fH54=_>ZjCm#Jwxr(jR8c&#nd;??ml5tPLAwlxCjEeDUt`&0 z|BNJ}#C=GWBFx|ytoTvgTG8&9M+pMgoGaldtG9>Qu#0pYk;iHVmj%U^ zUya>P+$n5cTkVB-jpC#Q9)c5Nw>x85`id8WHh}$3%CNnI#NFNtVchu&y)}Cl4nK?7 zx|l6>yZ@zTTdKdG^YHp;jEMdN%;C!*nqAhsdLi^#;2$mB^>)0vKQ#z9Nug5H>aqgA zhhKAEz?!oz4~J_r^s!4Fx?q1nEQL4tD*jP>=8qm)V!Y?_+DKv-D#Z36)g z%^m+2=Q}{W2@4xVS57@HP&ZmL>w5ikv(-b&F{|76?5_tiQZiE8@#wolFO4T2#kVcL`pMzhFyqgcihOJ@90VtC*u&wL7rNNXq-tWcs zdTzIM9eqsB63(JR$6e=?OzZwvE)aY?X`t($)+z_8Z5ds-I(b#0^&?p zNPM!f1?J~i&&G!A*ZxV?fzH8zJl~?$KW8_;q=khEL8xF*eZlj?^>p3R9-HgcBdcue zV*75H&bGe14C}a) zPlE`IDV%(SH7B5!f>#G?EJ87$j%+_g`4C%NSQEDhY}ZVDPPh{w5A?A`nmzS7(|{_@ zSy7LVkE?H6#VIa_Wy7%#3X^4# z8Pr!W<>Wqbyz~CMAfB)h>W2tAZux28UzHgEO+52%rAysCW zYd%_ws}sK<{RdYe0C2s$!_4I_`J0^;M&Lr|r3N&q#at>Y7Gv30n7W77kZ| zfX65&5O`;BWV)8yEFO#dl+lujzk~)qO)NF>4uB-P3*JyxtaZix4SR`j&>=ve5*7}| zcZZ+lNLNby$auLDlfvSIudV8DU!BntY-J!$1f%BFPvMh}al&V08f14BS?v)ZsVtyC zWsu|$P^7_qjrbW|eCvLH=%jO7$B;ym5~q-XMLF38 z6Vu!d5&7T5Mep%DNeMM8Mt4-yf0$Oe{gbk69lKaDvlDb<_)Yp=?lyP)JI87&BJCurhnTib4z>-ZoB6QJ?aZx~ z>z5E}pj!-qi`?)eH?oKGXl7o6U#y&70LeQ>ZD`JUm~52`4ZZ8$ze!7eC~)NR@`P8L zl}&++P^hlocecOXy_tx4Uy~J;_`JO6z0;;IL+K%7U1uk2+k22XCKu0EqS+7WecH<` z*{&aMO-+|=v&nB1&-bDHbDW{%)I*9WzCbQ&X(H1~niAUTDS$9f#-#<^r`efEYtVT_ z*yfk$dkU|A`=7E9apm3ABOIju9wtxfV+`ar1;LS9>f+kZjcDw6;!;3H2EW&qzA5X) zK3z$jt5YK%pu`R>eLahqp!HwWI4OU^E5Z%T&g0R zD>iNB)mg?#I&}2R{bHqt-&IeKMH3Gi;#{-z!Nkk@{i-3HXX2^cAg3%qW%i&O%npqq zA+^=f443&RSDK;Rcd(h|{ffdw$g|aDxrBMhD)B4u#?oJ7SIvB*`^4TgR$rm3UPWt= zOJc@9c%B?>X++J#nXbOd!Flf`gHOLVSD>g5qz64gi}~s4xsvpp094G4rfvH+Rc#n3 z7rp4z@Sbv*5gkSnSMKxFv6_SDgK)OK45tL=_+cuXk*eOFm)%GRAPFRd z&{Zk~k|fuR9@HJ&mO7|y*e(wK7B3uCh&p!{1z>h*MCRYWr}SBfWdktZb^ z^jezwj4UeC96eYgDBaqyp6t?OUg-!c%UvH*h~Kol3i=K|>N1JjVMGfyDxXc?l)0=I z8}bImqFM}O7Kv`+e?X0>dq>r^S=<43lt{r>ntO?h`em+ILhdwoqb8~frak{5S0k&H z&))bnjHxBU$qr%+ew-RpA{YaB0`WO%T~+<5ED_^z-QqpX1X(vT#$x$u<4SY)>RjrW z8*UP-8KZpTN^JoL0R%z-1Qc4YbCO|r%E|BodAcUve9?LOtIf=rWn<*m`>uw^<*{or z;>eIXN{cmfNb&&TTD9(_O_qwhhUIXGqg2Fd&$w1ZA!r!gGM*dAoyXeX;J?3?m2 z=Jn|Ns6&iWXfZW?GRD;Sy0q`JZi8oWE|g+o#mXuh*J#!Z|Mxz)gb++KDyF{#2TV9} z3OF@wc(7Rd6UY%E^NM^}(4Ttbxpea@4@;zt1-C*())lnVxtZA(So=A3?iKcNICD(S zDs>Nz3@T3%XWC^=k2(_C$Jv#|fa_vRRUvc&-$dKoBm*6(p*sw!_t&)u-Z3)CchUfS zi8|w-q_mMpYtYC?82Cn}-Q%?;fh)u#BQw7eEM?}vGi&1L3B3lxBk;3A{B2I+>pyE1 zR7|$W%GGr9yz3VS`euRGS83)W$3Ht#q~U8ayg>A*=y+25F|k@V_ebjBx1{%eQYZPH3ksg9OIv)j zc%<>Z*oWSP4!}WkHeDY7Kz-$&xXhSc$$y>s-II#x!Pj!~@~9y1yC7szx<%Uce?TWG z9j9FC);SL$jY6lYxTvfV=NaOoW|XycA01o5SJs%kgL1&V#SGnJ(;Qz}%kjLS&MQ|E z!e$y}As+9fRyF`b&`#whQ^J}j!L)yTfgq9k2ar7k%1p99tRV=*w}qg|I00*>SBUcL8%qFs>k;kK?Yu2_9gmy+E#kQ`GpUk08#wziLVl@YT(tl?^J8(s6T> zGP`vq{hNkBsc8JCltphqr6ADA=WLXc^2jI2C{R}rt4j76z$&P}JK|=af=HoBN0BtY zDW4~B=6scONd{qoMbV()>F02Pof=IEFHchoo#9WNJQP7|{L#ApD9OLf)rm{BTElqE z#18Dt?3m4#EfeHJGs_oo$~p=n0Frg^cI!Ql{2BCHIWok=#)h7I>FIr~^$bRx;LLQt zA#Rm=9rU6<%fP1c<9Ul5_HT#1D#3kj`QKv@RNTXtF5PHV`ub^~d6TJ$SW3r+b>MGY zzEdm5D}SE*^V-PJg4a@O1+t?{z99Hb)yYI*J3FiU22o@iL=Y_bN^yL4J91O+ z*(bKcSDDPHic^n^m&urD<#?D_{hiDk0!S}AU@Cvo?mNR>Fh^wMGiuRUP%F;;z&=J zbR4Jq$iL&VG5oMLOFzkY&NcTIGj^wKbW>4ySz*4-r+5(W!|Vlz=gjgcU1;bW#_PWA z!XcVYB(%d%$HLfvIH2f<_1!m_eqm#Ndl>F7l%uQL00jm4rM=j%#)09dv(64%Pk7*Z z9&OhBdiMt$nvCwVcM+`XK)(8Pu=#(0-?*9s2i@ue0-L6`a4B9<dlXPDe@?47&pMpQ!m4nS_hs2?&=F+2*i%wlg6!wWdpZQUIOf}a!FAz zPw6L|k0Xbl;flY080#ZwTY- zP3g#iu0Ug<%0{tv41FvS=G*_tENf@1qA51pcfrp(4>GtNGawb7%&0%I-xpN-BOP|n z@K`N7NyHF1@EvoLpq+%KX1qF+r=kO(jPsX23~Ftq{HR;>mmnYA!QTPLW)TG7Hm$VUSIOqFSo`tG8J0rK~}?~ZAeSB0^$urCz|iZg7RaIjs% zw7L7!e8ZFTpa<;GX8i^*!cyS z+@-feVG}-a5-vv?kHTR&fG_z;%Y^ay>k$?TAq1}+Vqmh2wC=*=k2N_@cLOvhrw?3; zrPdO=1$oH;XlCXN{|-WLoW_KMlMATPlfnIk4k!eC8@TkkP2LCVUhPbYJM3Q1@C(Dh zoN@8O1D3tGpKk>=rcA7tlDnC_=KapZch2ey+QY&pT^UZn9%Vf;*0piv5B@xqAFww8 zk7_N}ehV?*Od7aSB&rnPbpv#JQnG8VqVuL2rY~2LLrXFaQ`xS|QA)TO5cbXK{wK3g z?{=IKLtfX1^Yf0QP4LQ#hlc)}5<33hzRe|To24!T$e^OJ@{IApnP8k!RJb%l91yfs zAumvhJjzEE;wTD1m;an`P2p3*{mt8XO1z-k3lYRtK1x?>;<(_fmB_I_&%gd)l%^3c zrF%VU_dT?8eb<;VtdM z0>JT&+enU2kjwS_#)_*i;d@BHPzmC)eeZ%5G*^SmQsYim(Sm$X3|~QzN^Ty#b=w(X z1e0_U*zs=b2O2jl)0>(=9^HhoQE6&DxSQ6!HG+oCAonDYHQR309S7mJ_?(v`OV`@K ziy@PCYJTVrQJL%Idzp%D&tKHK(x%V z=07P+E=KW;e;oX-LjB|0Uq!)9y{-S71$Z~B9j``BbON8w-wN9D&zRCEZQu6N(isaq zJG=1xE9U>w2L`L=-V>ziHBdw(j9h4_- zX7sVG&R{&9;3&a^IR2eifFItSWJVomhCLS^k~4xI96v<;*!L;WWd?#R6n=^73;Tccu!+*6(%ZY z<;Fi^52Tthr>M+cwi+5D&`)+Jt$3Iwk@S3RJ=*Pq#5IQL>n)+zOQtr2FRjlj1?{F|Tt2U2rvvl|J0y;0D?a;jnBy`7KO-KWrHzQhx}tYkclU9XpU; zW=(~O&Ik4JMf2MVoj$~0s|$b1AShnzJYZ%>M|*fld<%his1o|cGLncTWK>Yco6%wC zQyuR&`Ur0Ov-W@8HvjM;&gB(%L}Tk*(+a7sOlzqc_rgE1;L@^^Ka&vvQSQ~=SU(4W z-gy)=^tT68*3YP3N{s}hhNvAOeS9$Wu32Z|_3KG+$mjUAk#MFG&nh# z4`xr^u&{iD#lwHcTWOB#lO&_fW8d96#q<;(d6Q{?CNOE##5Hj#2%ev!54fq#e!}Oq zqGRpgSKXW(>QZOHUd`R>P>J++ocr2jP;n#ygF!${1fn82n!qrHs(6b*sE-7b8PCHA z5W(sN5}85Bd`ZIF&Qs}0a3ZJWVw8QxsM5Zy_hKSAo87a z1LZTNsV8|voaS!w<)GnjAug0iGB(eTS!LtWd}6eK{6K)OU*V9Ll1N5{X>GT>;~;Dq zw&2^uWL6-7*^p37ai1fpuA}mgTkEDNBATB<{{pja8#df-i_J_lplR4w&Hs9UCzu6)dDG<q@n_+iUH(vE}19=f!Wrf*W61gHpCuRsz}$F|9W>{{qK3=+He2-N0;r%bk4B% zke7jErTXS1B`!eqv{9Kf2lM)9+G2dV)YDlv)xk9|RF_H2IcS;<3j9XiP~6ADTumKROgg=EdCuMRx4~cC>p^)rL9JR>;K3rvjq~+?2H& z<_Wusc?=_#5PKu)8m}ai%LjXV7FkIikJgQ3mXMEl?VDA%g|7ej$pg1tMV9RMCf(vP zN?yp4?wkj<^=|~6a<}FR-|+Il^a-`}-D*_Psk)C{2G4zMvqBgqKAG*wbTuB>KX|fl zw8Oegm7x6Kph?|&)c=UcLOHluoGmU<9pwH>oF&#vMq$DpS_QEQss-%FZ8x1)k_hgDR(pRjP(vgWDV9Vci;*3wR%&!^#oQ?d-rW%eHXm zQ`N@*P#sq&qxwl)X{@A#BXQmS2fZ@q%X;{iAOb-#B1>~|Fy6P&e>1wEY1S;CW@;ez zW}aVWh*~Yj!g!tq8gKf=9?BjF8fA%CraTTU%vn!k*Z23GNMB>ZE-q~0?Bpw@C>wY_ z$~`Jra7&1cgG|RRbItL=3g7Mh`IS_=-4gag<9}p9TGK1fI%HmmblQLfJ4MK^e&SDL z+h)}<%_F(#w|tFXWc$`rvoXhXqV;RB1v8c4n~`0@{r0i6@)GoptaI#1s24P^_!YPa zZT|Q4r9P5v$fK)RP*}{J zt+r^4EB}ZO#HlPhhBdGYC?eZOnm&1b5bznBf+HrZKE7%2O&ca7p5K(BG#AI`{8^xr zRK7F3i_*nsh#9QHiU=Ei!9kd#9a@E~fHM|l@72O`Vl^B;Sg(-2yyLK9t**qfuLc|9 zje_PKBjnCk|P=)$4@g- z(~1^bo!({zB$RK-XhVoMV{kD|kltn&9l>YuEPY5^<@I6ht=o)0+r`h=oZvQWvA90O z3EpS-7B|F*u{+-jzun}Dd>b5m;h=QtP(QtQce__u=F0EHjDwzCuff-mg*Sr0wxj$# zErErOJJlI~T%d6iaNU!9=dyIRB7Z{^zTaDs)xM_@31oFK|3&?UjTHLPgSZ|JMZJu{vQ!AmNPh)m(#l17}zR>94Jl> z{ymbl%fXo2l>15NF-&^S=>1-Ry!bF$cGLB-=ZgnsrAPWO*;8<%j;A)F*n3?KwH>B( zFDrzkzfF+6rnwl`nC&<_Q^xo_O4xuGv>Zhf=fmNXa*L?5H@_9)1_lQYa3S^Mi2wK} zyH~f<)ihAWr%xr3KJy9d`_yrxqM1+B{z@+f<-;^zag3TB5#erGWeef(rVybTx(P!~ z0rQVAKZ-r<6B)jF7tDK&?B6zkmom$~y=1dzEWnMLuNzsJh6|tFdp|q}lV7a}SSwlH z+UHC5!VL;@f{|t-K_zB1Y6epb>>SLrbqB3Yldieorcw^{wjfBp!co8a%a<>9Iy{^c z5@KAoAM*k|sb2~hRG%!~%zSYr<(Wfj@ePD-_qKuC4dWKp*5Hu=|H>d=6AJ?I7Y!&vgd zDvzWD$)tZXk`X>zH7N`hH5;Gq|BrZmiFQ`(hTrZ}wy$HaZpyW-(EY9-MzeUT8`RjJ zE_y~uq}%>!l@x;V==!Q00 z_u1ka$4XS7M%IWMZ;bp`U8kUPJtIlg@P%hAlg= zg`PbMW&E+Jt8@}=QVbS7L!7a~pDbhy-x$(~l4mOw9pM%5nj1m(38E81gzVRu@2Wp$ zqT^XCHE5}FibxUqlFt8S1FRpAx-s%Kl6W7opA=K`IrDMX+VxQY`c`aw>(7cF18luz z#3SU6Y)Fv%*-Utug8Oubyzl%Yo{G-Ay&81hQ#_k*X$|l*y8QRHEp)E5_QR)^uw^c` zH>><@2eV@eK_`jQAJx1+lY$4{4Kk{~Z`R{=BDK8YazmCcc|twv6A_D-};)dxdfS$R_x88itZ{2i90CwJvu}`UF~J z)DFZ}+-o&|b^HVP)ZQW@o>=pN)kvmyDSL;)pQ_wLVrlz$jG&i%m9!H7@YM)v#W3$i@Jl`yx|HIQ)hBf`Z@s1ECJ*1n_EnU*1doofwrI7|nK?c&@4Z`S_2BjOM zTj`co^6Yoc^*_Ag4P3jv+n(pXKe>YkW>E2>nDV^3~*{Gf)B2^Zus*m$4q>LrD$vAV7CHgG;qK z^J4tRWu3W?;OV;GxRa2o+Nnk`Dw_}`iHy5|Wx+p=Y`FBqYU6H?P1$#Yu*OV(m?!u( zW^$GejPbp-GS$nDv12??Zqbh_!7a_Wwdc8?)E$5JO&{y@EOG@HH1y1mb|TU@3N@Iq z55A&Qk{*|;YS_5>8eY7BQxyds=Zqv!(Dg~lhUCH%J zj-hK7Hptv(s=;x4V+ZQb)FJ}sMrv7+N+MzQ57Utr8Tz|{^_%0nS|Y6+4X<1NdS2ZO zrD800c)aht$}0tww=LpOd376*gQTsl;@dp~qRJJ9A_Zui*yW+Rf{G|*_R7f}AdU(d z+}&*rtWZDR3JI4nY&R`e94k&&;U5g=VEDlRzpQRyiiRyB< zpguU_96q+flx^FfZ>>{gkP*a0lI4%OHGSas&-}o0)R4**i*`yk^+NyFq*UEJGNg7D zXSbgyt{N{e1v(`_Nj^j7ug7nqZ`+64$nlzA8>^N@42?fTu5AIel@VOqyMn6og;#s5 z4pMALdZ>RzA_jD(F*N0j{3mL3a)aH026j@(zCEuorMXeE87=y9YBJGXv;|mv;jH`Jwb2J_it$UDL?0k zI|;A-qNsb#I2IkIZVutbVc|z>AV;wER9t(dkJvYr@0vj)GO~Kj=-j;H!yaKMedFOV zk+vh5;;81R@AwwPGC*VV0m5@_S_BkAr4(QMUvs6Bw@0=npsjjBqr40dpWeFYnw8F* zNsUd2$smlDV{K0t`0~d%-LmAu`sZG^lp`<{z8!`hN=YXM083_8pY2yxFdg*!F$j`v({?`!^JKL4-YR*IJ?kt}qC0}suES7|||;-otL zbEO}3?OGtRPyQ*Q`!OzE3-v~t)ulw`pa^+=j(ZNtfI7m`UD?7L-iQSKF@4f|?yr!# z;|V&aefn-2c!sTzqlM_j=0%lV3OAylPG6ONf$l@1scIii2q3i!tv3>>kKevD4BGP! zJnH`E^4#81#q)H!@kD$3BYs8!H^nd&gEjnxk}UCThHFwI$PEy721f;ADV5AV3=9Ks z0n!}^_Y?wBxr@|A-tO^it(@B=3TaX6`KE*gMi~GT3ug8h$_qUe6-MXb6vI2yq1R;kzZ1*={01*7 zQL`@;>i1AvSx-YdrU=R(E>|N)sc3I@-G+G{-X06rjOMK;exG2&^W57@GZ{gumJt6= zR=g9pZem{bl$PtlDgdS>c3i&!qrO|Or_lNgMr0c;Uk=T1hSvcd(hbVIw323Q#Nd!g z7N{2s3{7BS+wCtpK}3&yoP{=NMuYQ;0SM3_5aF~t^z&9UAccllE6UC@yY4=a>-&#d zWTS!o&gn0^oocp{$3Pyiyx{uS*jNU9>_{fn`4u4Yl}nXijgTwF=+GEa-{oaYdK7smjjD;q@<@snP4qKyqh%mycq;qe6nrw!3v|kiKvo!L!6H z&uo;{Z*D|mkwD3t08oDew(Sb|C|13tjVbh?^NR$69r7W9m%w*RyH-aq-L zUe}H1EEYe8GYxGTw8Op%mVcZJo#fP(N+hIvvyyEVJ1-w~>$p;FBk0*W&o0^l6%RIi zt+;vWtasf?wZ*Wh1nIT4_Zd^Obi?jKOBtRmHRLcu@^M2s{~7nP?WhLD+KRj8+fG|- zn#E#2LRl`x{Z6bP%-}va=LZ0oamE2=p$NktX6Dbrk*-bFtw?dWGI`5&s?H`Uf|=Hy z13%{B&OlaoeIHv~Ym)Tc!{ZNpTnOqsYU_Tkc*exe4Y(IyeROj1&w_tk@ZZ4RJbw5< zHTnZ{uV@}oj4)(RFD=&^`SBVw?JT9B#k}_};ydyYOK8CkbZwefjKBF)D|&C7@vH_k zECB#IVbL)OaOpl(9k2IIo5T_%E-!4($nS!d{C~QxRM)@z<1j$xUTx$3b#ho#bgvkn z9#=g%Oc&y(6weX$?x@@WR&ISj+@Cn}1=Y(8tx%7<6y94s%7*0m9V!|J9-muE-5)pH8oI}LP24`N-@bq`S;3k@;UrnF zU>xFh^Ihe!i|&}RCCNHPa7?1D4+C3uApudt{`hn>9SjO67*ufsY~OtZ5+V6wN*)M>TQz3T_7xE4;SYZ4yl3oZlQPdd+*1M<}I)!NUCey%YU zP}l;qe9JwrzYR?<1c2OxK+}Kt?vtu68vyaXq?Xxg3S;kx*r zD(`PkjlmY;E32zipf$gvDz*|+-y@?6{Ro1gbEdR+q3{TNS`9Xd`=v>Y;Fiu4VTjd+ zJSj>K+U*9Z?6-frS;j!NP;gj0cHta@(a@hUvjI_IiYh`E9Ieq&p-NX3Vv8SlVVfiM z$CFoLwf@%D@+kvF;IN0;@}IHa?F7n;g7w>Qy#IH`62Z})y;`uLdBtnKV6 z5`nxb;v048!?g{{4$b1xdLAAeTcDgT!pLQTpJedjDLnUODeQpAI3JnqxaXVe zd*-BE$ISe+C8E_86%Um6K*d zx6us7`YkQrSN8D=1#suf3`Q#KZ-@cAwxrVNMiwRR_zXX-Gq-C}Hl;^hv6To z@{?#ssB(8EXVkR)MoLT0o=z#{tp+Esbv)hNctdx6rA9BOzX2twq})`rX&!GZxBs~3 z-OEPLrFB2dxd|GGw!d>&73yx9Co3Z8ozH2fC{*?2qhSG!PD9Zd5l1R8`uXGcZf@Fw zCma6L#q2?UBZBk`B@dJ}RoJNC{LM#*kpj%#(1`C9b4TuPPjA6s#;(7r$tqOwsOUmD zN3*w1jG7ZZmGtvRX{B0X<-*f*G6)E3u^o#-EZh#z-}=?GRafB;Rva~xrYJX$O_Y!! zD`xNCvS{4gKUxmPh7&ORGt-Ctas^8J-<(z)c`R9{q4X?|)WbRRkY$44grTaM8vd>6 zB1L&CWJeb7fQ0`|*35ro_b3NQ*QDKOnf=DRMg}_q212*K7$yIPv}i= zo9*$SAnbZQt%#RD?~PwHoP>mgjLq9*Nt$adYm9Mh{4^u!Krz? zc}Zx+=KgH`*h_fwG7uR#x^@0{b;js~K**oQz{sd!#nL~K@6U*Do`s{SHbL+S33hNN z~C9iOerFpRZCrd2h7RaWLYj^`RfK_nM^h489`OFR9MAh|r)yku~NkVh7d{ zrYZxH?6KAzGWfiJ-+x=EM6EN#4A^emk_PeVRYNJzlC%)uzmDHbNH;@^O(yhHiq?an zM1XQ;UaN*oooQ@sqg3J&(3fd`eYC`niyd8O) zVZoG(8|zRNAeWI?FSjX1!p^&GVj;i%2FmnO{zO;~2-YUBjDjnvdFE7p&`Z`jVihc? zKiAZGC8F^4CGiW80Vw3H3cUz`6+sx%>7B`n-Xdu-!B3)`KcvPhXh%p!`_*i+bvdByvOQWq#z$+(C_P=~RLlfscNCFzX&%4% zy9*GC(u-u}`;iFDvlI$T zmdzoFHp#zMJ)lFPQK_dd#A|GgWOaoWL#PJ`Jj&ClgA3on+UCVj2MQJQxhzWN*K#u& zI5|dV*4-#18qcb-H`wnDse`@F`d8!2#OBNpjZ>jTz25^ta=;w97U~Z-r(iHe4omrz z3bBz@cPoyAx~ApZh?Sl$rvhQ=SD9|>KAymDO$SG_&QvcWm4Hw}tyudIto>jDx)hk% zLcf1;xqt@cX*JmA5Zct_i3-q;0b{WizASN9`0iY6SmyUBS+eFpCuj5Oc3y^hY;DK{ zLhw!{((Xquj5=R`m2x-eOrLqiZ}*bh5GwhHH3S>S}I zrJJK-Qw*c>8U#UCKH!yac1KZao9z2{<(XMs8C{ywThY2Gd7#>W7!vk3dwBk)z_Wc= zH!&2o%tA*xAHEDZphKa9t3*?TVxspL!ieK-{Mm=3=WQ4i4C@YxYM#*_%8Z(4bLECpQ_v|q+xwN|C&ZTScz8`?zoN3pM(Y|J7OQL7fZeQUD{Xw6p@-H7;R*@6J=b`crK(X zZMGrj|6>i;+B;K|$5r)W|3hm9V&sy_@b{~)vvmsmN*wleDU>CQGY7RKFX{|Ky=d3r z=5}H#6aQ@8b?EsD8y%bsPrN5SyPx9OEYNvfh}zyN&DzIVAY*+_U$tSMGRNUk2_`Kn zBmoHG3CMeAjG+ee2D_j|KN|rG+&g~b7E4OvA|S7VwFf8m3% zmNFK-CQwZ>6kLBh0JBpY=ALQn5QZ@1=$Rg$MERqniLVx!)wR5wQpbR z3km%d+kX_7q9Z^#-apGZSn3}S&VQQu{gj4SSpeG86d*EcW&>&L!TSbD?D=zGKmlQ* zcS>3$5Y6k=jIgarK9Vp!p(9z0mqW}ULuC8W(KLp#=@cO%wdnu zf4j1r{&$5v=f%Le+x837I<}d{?Ye2*LZO`L9cdN(BX~4oe-vgH3^7h{wfK1O2c>R~cgoy)ei_H3=F|svwG!^z(3PTIFkZ?I+fI_hIMhiH<5@yY9aG zrfIlLsoYgk#}ly4*QuC+buKXRDM zeVR9XT#}1OTUb!w4V_#dCbYWe`L@}{V%&QBJqMXddSBqO{v2~Zay@5&DcA#{!!~gJ zbV7QZd$QC2yPQFhHV0tr`}np-!J*qab?im<0llX=ldb0m;>|8RBOYf`8<*psZf8An62XMBPBS|s+0$9M z*JDT3?WdDZSCbX&#MV)D6nK8%6vjX6X>(z03?F!PC*0H$;^qFpU>;^X4WtRl2$LGU z1(G7^B;^CcS8MOf25ENk`Uewi;c-MnLr9IE_7cL8)IIz;`4 zhwKH2!;1^T*4l61$l@C|sq?Y=7?JGztsj~rHU1cVuSu^OKuj9f$VvSYD+=qvhm{0ZK${?;J`)bd#zXF4 z5uV1keg-P<&rXf!^L%+e-q1d;i?)3GHiVNa0rp#XEe4*3XD5^*?V;*hP>^=eBEOa< zEGE(*v;l&S7g=_ePQd14ky?oHc8(sk5P(X@mRTYp$`Zyd3+IhuPxc%i(=+xJnmg@^4o@h>nd-gf$EWptYr5uo!Qo z4)AuE+sS62CFu2#!tKeFh7>M7@SxB!p#=9RK_16AT3HLhx}dy~s0>~d*Wxp>?-xvj2hidw&i&s!@ZR=H{k-PU<^eSvxLTqvAPTL_Pl>&^=P`ZwO`|WCho8Cy5b(2B|*A!hw<{=+n+_nYC9K zwk|fA--&!TCD0I)i*opnpK~Ve?(~T*7zr$Ff46$xwN%a6eQ|-s1xnBH<5!8=|>9^q= zQEBgcQAzOyEVc)qZp0qTvFn+F&FEgrY{~)wR!EOqHk5sICYB~E5kV0eky;4Mkn2O# zk4Hsk94RFaTZIC7$$xGFdi-_O3`pAE$9+yqL{(`S0Z0oV@^eyBIr+$NsQj_f;$Vjw z(ts^KVMng^$KsVBxDF!-9PH&m2OiM(&c96M!Uj{kWvUYe4)lwGi2K9f+}}1)_k58r zbYeP#-@teQxkf4oFaLAi`-Tu}`4L60CWs19tw2VSa5GPc?!hzALg2^C%@td-CxJZ! zEqOa^C6-%TFzw~%8$SuyH3Tq)jRN3|u|(oXfGOG=h*dy<#6W%ZXfibK9?zh-`IHH) zs_fAR2xUVB_(ou@ivos1<@&-i=)s1f=0S@;g%oy|WZ7R#@9x~cQa1koJNY>Fe&C3RkkFT?0@WK-anih^wM#4!= zSP8!r0)ml{h)v)Oaa#Eyzl)y`|K!Akje!@^SZc#vp`0>>+?9upcCvc9Yrl!gQka%x;IdoCG#UOte|YJ`#)#EEB#0TVCNGP+(b*Q zw^>F74Aoag|BSn;S^`C(h7_{XOCwu9giqU_GB%K9;t$xl_t`uF2Vas9GaYesFk{V& zTjyM1AN$%i3TlLy(a4)N?tDw2Q?WMhvtHP8R{OoQl*e79qyo}>bIL$(rK zU|4CUkK1|AaW&oN<@Y(Lj3IP@7j~^a+kZ?u_9mtL?xl#*)2g3;p9LYSjUs5fKJLBK zViG{(mQSa;TbAlLS=hMYiOa1j3Hsr^cGcdEOn`QK2#YQ^m;I}y7g{Z zxa-%oCNka}5}yult!57$_J8_w`*g=6l^kWk%vlMm7b-aG`c=B|H>brXE$~t=FubVi z>1&`l@|!965*!j*ab)y-wHEZWE=5DU)jM|Vm)JZQOsqo>1`>vwfL?u)=B$mKz_+SB zDkbeY#;pl@d^x|p*NwLnqIBv;UQS+2F+c5}LKSMvV-{n}W~Po=@gx+nFw zjrJ}ta1-<9BvTt^^8%h<2h`?Y9=6~+-0@8|gv0|0k_zj;Xh-h9UogGT z)Z38NqLK2ndELF^DRtKvar*R7)w1sAM?K{hUzVBNU^ok6N?G&2y|K0V{Hgamb#Dz4dg8~}m&0nPeQ*A0r)UnGY@yy+>UyemsCtQ9|&k4e=k?|>`To%}C;ey*&XEQml zZta^(3M+Tw^8YeXJuSI}5>0O%xE@127H@nBW+8s$$xx7wPS(zep$r;Db_DWOe#Z zzpA@gsD=1I^>TAy9;lX;_8(TI3lO~XzU!|*XP-PU`1Ya*d*E3Ew9iybeF}RG^y_=0 zFB+@7C2Krs?eC~@y(*6~$tw_!VrYcCa*9tN=14ZC`~lM;e~lPV{kIdtV+xMmdJfdz ze})fGM&rSOpTvHn<3b(n#QAyeomLoui1ajGk+RMHU>h}Wbl=P2YtuD<>gt5@=)TX>DS5bMX z9GwhP!a`)~Xv2umWn@@OSS?za6ZGNI1qzHd&~WzHhE0^@42YJ@PyvFuyz?t}Zl4AL z!1WlyI6$%7S$NOMtH7XmQZ{l)1L)Hii#R%Z1);)=SS)p_E7JW4phnT`t8;E4y_gW) z@``y^_u=m)iP-|nUR9HGjC58XV2V{+O@6kT&T~S>A6-ABqu^nN8Sbd|H-;FC`JXMG z@^35XuCH?w_8L;KpU_OAL!6n-;Wkl07_XfOv}3|kuW=3=fvYjU0%KP+&V=nsV-h7v zDrNmP`)Z}fG^?yrP{sINm(L=v(4gQg7$E+$>fM?S^O|C#A^}svKNF!UbnI4GZcHe9S$aQ(!o%UpE2w2Lw9Wr?#{J}ACyRuzezuv4)(e}u zVBd&k>y>$PVYIa=Qu&=uy(H?EJXOI+0b%3WC{L)$IIlY8*DP7WgydQ0iZbHi-r0YP zzyktiSh!jgK|LqEQZ$y9Zi>)LCUuu&>w}IndsD|DN2zjHK1s%mDhlB3kjXQ^h(>yn}<2dp5JK5H7l43d^q!OA7>?!%T)T5+k z@BwE(_bJ)7YwM)D*&U;p%5tB#y65e;VZrZzfU$ZK`fS7owkO z{jSFs-ub#X1rVEH zw(c|V-fGBAnQTRmN1-E##x@HROLYUM&yUn?W8dGHJWOl^EreiymO&Z0el-}U9pwCN zEA-dIn$+!4x7cyJ)cxPRLZ7n^b`_y$fM)lvJC16fpYnRf#$|A=Dmpx}2HJ;n+B2Rp zsr!@~^07&=`1;CEqYzd~$AHJ9=dEGP?x#&*n%tpE*E9K=k8_$&+i|CVq}gpJxpl97 zMpx-Z(c0+1wiDUPY%lWCV8dgfs6b_{_;GEY!5b%@`>wqj?p&Wq|8-+5U9oSwnH8p& zh%^5?e&>fP+cU7%EP5uHKsjN%%3<8<-2yt*Ago)8(>{Hr{v0p>uj?n?%I#v zO~8g3g>y9BUx2Fq-2YE1Q)OcCVm4rwR}bcqjTU9Uh4nfkx$biZ=^^VF*>}1DECJEM z%(B=f{AvVC=la;)UEAB2y-e@b{#Zm$Tw3wf+5SLIqcj_4P+9&XDyrvjDs(*uBPO#WUu zpHmr`R7VBKUI2hO=F01s_5mNtVlo7?SrzIKZ_v0qZ+9ilRV$h>-QfCJudLDJo%;B_ z!(!a2dg*j3j~e|KPLns69cqXZ!zI!@1Z>hStZr#J?4DjtZsdWspw= z7wkIWxGUf6XQBgAbx9(}YO=C#0i`7(q*b%K`RFf>9BuI7O5iZr6mxD23coY&)n=d*c^){03RkEb|96 zY1dW+m>KZm6VdfcCG1tee*3eNnFsd8%h*WPQY?b-sY($*|KS&6CIycPlt}MbU_&5J zNgHbv`pXy4C^mTjU}3S=qx5ZRi#30*yUtv(9dWE$AXE`!c9xgKX(9x`TFFyWjzF{7 zH5TWtJv4?H8@`LK6La^|rXQc2_{Sl zsSLYjheQnO~ye!SO8dW&dYO;<-wD-DX+f|Oka#q_Qm9P$xSp31p1}}jr z!$%$h5eNy@6Y3^FjkX)0XWmP^oU+XU>rJfmO>8WcQu|~|Z6}W)W|E->!kl#c?v`Gt z<%qON?$TsGdVWwO!rquo8(~jqY+zuqi`=Phd~RmnCkSM!{k|Kq)-%y1>YtX2$Z;3< za-zkH;OX8WhA$pnN9&AUJ|Jh@hMZ<3I@5U~vHkZmWmi^)o~|CsCfeCE0k{hp08Jbd z_vQjv*oExZB~74xj?Ak(x{G_>?}Ur+i`O8IiW1zN-}^F+*4s+mFM;d7`RKwH=I0mF zKNDpITFOJ1M!o3Vja1pHVigf$$emr3$*$_M_EO|f_x5f%)17;3YOT*mO^lA6KtEx0 zmVh9E0(+`hKa$YS;_dP>r{IV>i1+(lnHH>~Tyxl}o0GtH&!2ZLY1mF67I{aNjEL&Y zIjxZBynsTdSAAXAB@ZHhvgmm7Wu%i$1AXi`9#6av+mO4nNA{us!A?LCH>ivh2E1|uN&u9Gr9DyYhVD6Ujrab3 zN9#c!xlGMMWR8QRe^_ZXbiGva>!bpyOOW%moJX|UXE{S*+7RY`CCEs1}j z1)gP$K|{T12(J3NQM;kom+>S`(BU$(3m5)n+~$fc!DL?6=yf$0r0K;FH3 zXS3VXgj@+oi9&K1b_+R+W{alLK{4r#|K6*Xj%LU~c0UuiPVD$jM1SfH2I?-%*RRrw zxB@_V*Ogfo(a5dMcDL8;Gra2>8M*GW@d+O^{WkBy1Lg5I=~<=a!)n?o=g0*mK2(R`J>b~MW?Y0icDj$2t=b8h%PvZ2H?1q_m@;QB9n5XM+erWyZxDe3F*ul{>( ztiab)H=y&`x-IzM9yI)ZvYNIBC_v<5-%|33T%6C)K%o!2`Hb%KqyH_l`ho@(gq=(5&=x^w(2n&tMQ6bu$T zqn+5FdbMDa*`l9@w%mrdo1NG2xEgXtqJWG{=&r!NH^RbS5k`BY$n1uMBHn?=d!AT! zJ=1^~y6iW;d|`ijdhd0vk8aoX=I53gRJ8>acvGrW2osQiAwqY~0_uAv&ui7$K9ezCWJK3=6xQn zupb4;`pD*57+PQW*sSTI1ElS{&&Y`Ug$}M8hOK@BGR&U4I^lmW(z-hk?4$jyH_Hy7 z^Aw(dNch7v?W1<|v=^Zj6nv@hBWP{i{ePsG%{|_GxJZVYbNh)9z~{Qcv=bS6hg9S2 z@D7NdezSc4{{3JqH4;Zrb!pv64Y979c>L2yS>1(f$vb!ZM2-V3a@F= zh>0LNQRkG|_hT5w+zZ@zOVA~a$~|lbn@xSTC|kw=ef=&UuPq;a?n+>ClSsN6IQX@oRGwZwiV=?<%vij8D*t6Oc?xZkTN7++QqkYiaX=dQ zk9y_#Qate1zgm*cLACp-LF#_4{Zwp=KD;N`S`yRz^lQ-Z*P0ax9Ic?8S1nzSEkUiRG*@uOZaMVd{y+ZaG=}<@}iR{7}%{b^T{% z*n^L2Q8784#6#A5cV{MVfeRo3f4Y$5=&#{4sh#(x_lLqG*G*W`1gOPjSUp&c zCdrqN^ku1A%G>M68u4{Y&GOQoCZ#x$$qR&i2@CHHW`nf>N4jr&vbDtiq>iTyL^p0L zd$i%>8=5b=6P7TUngvzm9EOB<5C{W5wwbZwSuz=XHD5}cCN4wNo4zCyRvfi2m%GPX zPLB5AZLoku9=lr!LKVzym?{xiOYF0ikjxM*lw^ItlsF-rSImh3hHGX{yMlzGu2D;YP=V)V&#J=>Cjnmw& zdbK37en-EBLhN!nFeo9& z=9s-bVfHW>G)&~D%Q6uSw#yT~#MPJ;ys!PKaN?xMCcOo*nHG?Zc}39#;@jnttDbgU z4B8np{`S@}3xqtesB!0UaHCLExX{uFT3J;ckMau===NtdCtQ9Oy^+|Vd4Bbl(qky! zzW&183%(zv{DKt#WRynDFZ6O!(FjQl(Gfxq4ka^Fk)JO3qcF6@f;gIuX+oen5`e%K zSX|14vP#xh>sgAe8E-OMqaaKlUV_U?%Xm|LhuRDVjUyNa$dZJ5JidydzLwZhAQx|} zoB7=6MmcIe6Nb@^A<+9dG?Y$2S!yvQ-TRZ>$;-hVQwPfK=$t=xa_d&cs9d5B@xpar zi!Z&)lT|h}FxWAti~SFjU;>eWyEo8ce;$CePpHJmysWqHPg?i8`mfKfcXIu+Q;qP6 zch;P9MKXbuM3nheznt9M8*x}{CZNzcn-U*LADIIfm8c>i!iyR=LXLy0CA<>x_|)Tv zWD42}SYAXQyT=;c%zCkhZi+CL!=@LtvbdPHj&AP(B}~$0MOg)H)d6PG^yq+tW{cH* zeMh}IXQF~v=9Yza@3LNw>Z3yni)M|VJtBU*;x@lLM^j@fmpQ3&jmZc>!R{X*W%kU< zEn|g9(_un@biqUG2_N{Lsg)2Qx+X$gH8wC~-z4 zlU26c`D}wXe;PtL|FG^4)*~5ilnIvJBuW^XNfAa*j+2^xYRX%j^HQ9-!v9K2?Tl^- zfp~O9=A1QG##%Jyd2ewOXlpTog<>_UepPmYJkF-LxM9TQ?U()Ba&+K$@)!L&^o2@| zJ5$6RD_<6?!;4v~1}#+)f`SAmGM0={YA}$y3WxPGI}x9xR}%N!Z+^7 zh-?HYD!>1k91tqJ4qW|Aju1+XR6Z0-h1p-a|5IipPf-Z&8_?z%1$ds2kE4U zd123UmrhqtfA*U!_5r3#6B8}hvthr{w7^u~inL!aKAw0z*P)nx9rOFAGX>HR^BiE;RqTtXtHtj!22qq$+ zxHwXguZ{Esy-V@20e7RpWlZy3<>+;PN(N45^*lg6DuMF_%l||&U6)z^zGX`W z+{KEYx=ySiQ5t0T&pH0nCO+cuzoEbuNvYKYURT^6FS-BygG_Zgw#>Vm{W_5?YAGyh z<2aR!hqlyNP$+cHLZ>W~V4<=Z8LXeB-^jBb@Z4=0(faF8D{}XDGJSw<(wn(JM{Xte zO6hw>5)@C{&${jqqgbd~__Ae@0Olm+=2|YRSdxpsEH<&AS+d-R=pW!Gj> z(raYLT~JH*=lNm)R)U3bGMyGP5WZw4jh;+sZipxWsCcU5m^f6iaOZIl>QQibrkQw- zzjgk`9nUHGaQ%FNd1h#AOr22l>v*rrZ}ML1!ywP&p9a5sZr_1xJt~Q-+pX7?Q%sFi zan})DHzoKF2mT}r_L&*JSBvM~&%^hDw@y<0%GUTJnjbeigYM6}$~m8kCN^`&xM;ByOJ_um&A?Uv>@e@B)^%%K(pU&{yL z30zwLrE>6eU406=>C*YB8nhWv{mt-8`qxUE2qIyz*kHR*VRDUdTJZ`eNO$u+I9@~Y zxc}|581}K#1nKJJBR2QKnW}6B_B9J~Idg3!a2RZ^5HL#Nz%`mo7>?7y6ZIXOODv?6 zjAzf=;%$#>PoNDV;KfJt*N}tES^b>O9XV_h(k{3~E(ORRTa0rUd4f-CH%Grq6~v&47M5nu@ksh zw>97Fq{YDixFWS3po81bh#p-FML66uT9@2u6~jY|;Uy}ZDFE(swzszzx!)6_+Hq^z zby)%afc!GHRYyFML*5xtl4HtkQ>exuFs90YbWZsa^k5OxJ|uH~AM9x?yp3DVz&<|R zMOATFOSO#Fiui1HkNVyczi{pHYZ+>JC@LEt9U8AiSVvxtSU_k-i}MkDpeJ>5-}z`a$Za%I_^%BkWK|w7)bWuV*});UCZ~i z_8iAii7KG8mAt`U@j78db$|9bo&ue92IME}pi4()Tz1gH;MoY=EUb7qXH8WpNj4v3 zV7FCfjNYecADXzSzg+z4hfD}T<`6y|w#~xC=V7SqtcNOfx@62Ds#$1E^igU4+eva} zh_=mqZ1$oVxQ^+hFc}78Dwn4yrUX$DpyJf8wz~eBFpe%*Pu1}e$%cjZH*oL{)K}uh zpuQ4n{MS7T&6r9Ai(SX+iiUmz#jKl4P?~=A>aam`8l7t*uD4s>G*f$N&cozMln}+S zw|Z>0hytZYw=pp^yjYj|JN^k=?{I&^dfn3fctY?!wrVBY-Q9=;UPczvdHdKwDywf| z!t{Ur8W5A`2B|M_9-m(9j@!=4nkes$>n1nee_f;e7stV~xFb?cbfx)Py0>>IjlgC_@--w5VrL&-iVzlHK zrf*Z-UZ5aFWB~v;9b@f%D7Fr~hd@n(%{MI)SSANSZ`#DQJ6Wb!#FLRBFO%P!wnYi< zxQ!f((t`C9m;rk21X`IiDcN=irmskvlU793MncI0fZL8g%A+g@-wKM0kE(U9Yr!SbZn zvrbfNgMnye2X_WW@ELXC=P4v8R$2MFLH5HsZxP(8RS}{J$;zQ#&af~=jtIGXV{a%j zbW52Fr|rgbagaj&0m(kNeXD}B z5P7)-kFPHt`xH&H4(#I)*#Lrd6m6nm2iaeUQG(||6y3~TW1pid?9F8RkPa1q(eB<` zl~29W5L9vKJ}xHQ4&FJN5b31bvWiiLh{4q&HF}{?*yvTN4dZFuCWKJo*rflP`t<|S zC`94Cl9pr5rmXV*wUpM}u+{9JP*#J}4ADt*y zkzu}z^a(A9-fFgN$Us^)0{x@UhY~Eo7uyciXu*p361rb1<5)mqR}u`nKfh|O97PZ` zpRP_D)FG9(bARKKXATb$h~+;^^3^ppAdnJc=wtgWJayrnr{_HOWY;-4r13V?*xCRY zw^()jsL_^ON7GH(dFT+lb1-y z!dLSv=_KVuRG;{HK*Z`(txk#-f?~Fr+96 zh0Qsb-Y*-sdv)##*0lcmOv&ATTz|6CLM!2SmGobQyyQ(C5;&{?BsHtGD*G{1Z2n$j5?*cZG{1Q+-u~#ri zkX6Id`5>hKPOQnt%fRd`zx(pp=ch~OkyE$dg2YC0@yjTEodk&&j7;)|apB8OGAigJ z+gJxu|H_52f{U6a*Pq-B3`QH)p{)Y@<2-3cEstxYCQouwe`cMGZvPKWR~gmh`}c23 z5D+%HV>Hr@G>nvn(IqiD1OaJKVw6aCN~eHycY_E>gG#qZ_p{%5&i@rJb`EFTxp!UP z?QidHv}cXrM@V1CB+p`Ut#JsyW`4jm9^dH50}$Zm=KP=}fu zVa4)ERWD!IxbMdw($2pF16o=;XUV0z*ZC2THVH0zcpWX&!0=nhL3D`*b#&^K+`_G~ z>rzN{&UO*%uoQ%Hw5VBey)~MEl7y3NQsCvzd!x+UcB?|2D3J+ z;#|5;iAJudAq+Np^UI>v!{cE4@&Yi386=%Fe^uL(^7H3UZnNe9D=Rd>-6hxe-$;dijjTEuL>no=ORY)zeL6%d z<^9+@lE^ZYxL?MS>vMd2*0dTp^j=r;cDQe6DpxA#v3*0w&(CkZTGH>nUyQNVo1XUe z_t=|&qpA^*R>SM(a5$W7IN^~G?lpmV*|W{Y1Q8ZK(~v~%Dy>-)iI@#oO8;Ucga)Gy z-UxXJcsGt^7wl#|S!~f^IrwzAJ`}i#Y+J<*zNlVbSpKlweLSuR_-dRVr!`727~GIu zeUDpz!@R5fq0bNH8PibEX1>>M;>OeugwkxwbI<)3Us2o+$$qj9JE=lT*)7S@S-nm! z-s4QE3i4a~PHWPlQtAqZN{SA~02q8_A($wdsYWV{7Sm(;Xt73fp2Xw;v@%NGs8|{- z(3C)(rlhR!1=TPAd7ja6Oz`~|#71|N1YXSLT+$b+q~8gVSm6or@TJ4Pq-Xj#0$hB+-M7LGH%^l~TG40D!}XcFO2QTri8NfIkRv7`Mmxp9d^FA z%5|H**GCR64aP@$ZFB$qxl*P?XDWY{IU$L(7i?y&KVCMOH@p45zd3_@J=|U(neBj+ zz^gWVJiKSzmO+C~%a;p@sVJ;@%^!ha*Xc+!I9QGmU9(E|tM8o-vRB+YU?Y30Y`ODM zu%_+y(Y(gvaj+)fh`OZY)%xuwixiASas=G120`V2UC|pJ{-;e6eU^xbrz!V#I70I5 zm+{*D1u3i~=n?a2Dd-~g@z0&q_0#>~)I-Hi&Z=dv!cw<5r=rx;WY8t1*@MyHmKm^i4{v8QKFiDq z*u=cKb`NuU6GJ!gzk!%#81N zB%_yqpIsLAj*V7`x1@t9zXnR>MHT3vYA8Rt9>cr?9x8%*72aHIUETKu-Odj3e3d!qfb!bx-BuOzLiO z{o(!#)|OSAL}y#j*%7jP?BQy?siPy>;6qq~4bfjy8t0o?o^NA2PELbeNTVbMEwpp@jFRo!DmDnJRknjmnLCnXcT?7Mm_ zNL24&WFpeuAtk;Nw7pmo?f%a4p_ke_2Q}Ng>(WFk1|N@PBN?3FY})1k=VWykSup>k zSdp-^EL>Ae0=5ijHrU>t-D~hW36$5Wmbf}RJG=e0vN+@2^=ECO;Ua?e_MPec=}M$# z6%qaF+r8fN@v+ORt0q^I)wM+lyQ9#s)G9Uer*8eB>GM$!{kclkzAzlp!HrXoDG3%f z!fBrVGSED8E)AbPSVl=q|-q_k2 zrTeXABtx!;tq200Q|+fmuL&oT?=};yGvLrx0V*JMjg3Z0J>4SD20potkN+Z}W!uVH zE=B<+o&|$H63C|D7Gu-*`?hdI$<~E~80}+lLkmD-*wZ2lb|zn0t{P*@jSR`YTZ=37 zcVmr54^(~>jvVD9NFLkzfY^S)!!MMMU~E|AV<7OD+$Z6JlQ3j1Zf`4S#1Y4b4vX$Q zMy}oF;sUcRKdI~ke7nc0jI^A}z0SWtMGCB3)BAvDKF(Zfrl$hu!?MkDg7U;XaO1azn&z1xGzK%0-p z%h2HP@%FAZDMySUn2iOJ!NSnhM5}`7{r3Z!8#jpu}5+#`a-Gc&Ht4Z@H zbz(i|MJUCDA$a01BXN zYkQBB)Aw20Iw>}y*a+5)(H{YcxBwdZh#t&p#OvRtvZf0Kul#meeLtua^IBqAx<^;c zG~-*&=01LufCt`;ndf*|!l!39nF*vW zW_1wnx5v}_`X1Qz9DHnE!M7u5d{JWpk9XE!L~~nXO{DU*rJ3bXC+~;E>V}6TZ?({_ z#e#ND%Jyjk4wl^~Z8e_IfV7w)=jG#*Bjkm0GxLNhDt`UzqlqpV8tRJYoin_#qa&8L zuwdNctS^1zTh*}Lw@F}&^+R)4A_aYICMr3)3sWph@oh z<1+xS>iFcuZ~B{ko;`)M*i~J9`ThuDiMq2M%yVmW#(8kWj$NTs$je8!98%ZR6ww_g zahb+WMKdlxn*^=IV&KQhw;p`#QSg_%@wJ2J3~w27t81oJ$3*VBHDZx?sKbBNr==4W z_NRG}(9cmtIi$7J-yjh@hviQ(+qbX5We2{jim;q+0V5=W&MF_u+v?tnq5(1b$q8_f zcupE_VqI31V*zaB{I;Hvg&e7BFj(|JL3w$xvs_x5VN6m&%n%bqotYu{6Tf68HQk>` z!UOnrbR0WE$?(%sW7Wa-FN>c7g5XF-h^^xenHP|5tb8u7(_vy* z19~gYK7s3m3LxW$wB{xz)WC}d#vdi`oV8=AG)TeEtl=8c^mq_ig%q`cQ%8lJ)i$vV z8MoQCiq`yc%Mv{qz(8Ou>kPRbO{h+!-7GcipwKlHbm)558r+T|0f}-IA*ef3zqQTW zP`jr~N~s}eVqzxe{h!E2XY;4q`JF73$lOW96MQZJ%)#bx%VPKOUqXv zf4CGWN-|VHe!t=A;NWyD&eyNRv)BwIYh^-zz@b@3C*=-0O|hHF4&RcBqq5xZ_VqhP z1%igW{V_MsJ15myAtM9d6Fx z^wf^!#y8tj{@XJEkCcdY0TS3FmG_xi^+xS}*IHe^RGd|BJ)bymDl)l`0XA*9Wl|=Tv|P7RJO^F~ zChoZh8pK3rQ!XctSXZ&9-BcWRBqIDb3%7nqUp9JM(y6Iop=YSUV#|>TXsSHSsG@*G z+=E71p6rVT_N#ww9ujuXf;YAjI}c~(KcYl_Ob^(}y(cWc-Te}WJPV~3YRaQHDz>L2 z&y7P)d&6+(LsPTG7?_cF^O%K(xzC`!EFOJX8u_}f!()`wP%~*)$)#V=*2$F6tm4y? zlAe|c%?_HTq#3tjozD$1ay7cC2Pix|=q4WvE)$+Cqti?@IC zTNPS~P^Pbw{rceJA>!DB#hODFr;?ly+|7pyI_4q~#>Yd=CdQ#){3yTOXb&g^e_?cH zjddfh1nADRvDsx6R0rXc)ELj~(Oz0a+l9GBGQE_x=q`?z%OjLlm5m~c#s*Ns>zmf! zUj8Er*7;1qLzc*|LoAUV$7HX={|pU?^t;1hk7h2>e5T#aB__auKjXg$N)DyBW%|~O z+Q>r@>-7&ZMzv9H|G2{PbcF9el^ZY=*Rc_DcO3i8cmK!Br#srawxElewd=*8M~$G< zogl3jq|bU6^4L+dLW`0^F&bhGW9W5@*2YU)5G%}1%PFIE_*tK_iK|1bQ6NyEO)4cY zHZjR_g9Zu&-REb?dbQir`|sS>AGo`Gw{VbkP(>hYA~Y@jsa@!uJd2Wyb9MZ zGFV^VZ>|S8@#aPtAP>La{PRuRChhmTu21lEM--#*=@%m!F6+%fRyO563KwP_V*~pV zSM9!c`-%?E=s3Y-VQGI_LG_57X=Z_h$)T%a! z2W6pjMC$ao2wmuqdEpdC2p)0r#@+BIx|pO}rK2Q*!Kf%Qy2MEKs_N=7OD&?eud>Gv z7Txte#`BtX)Ov0##U|}*%4VJq76we9UFC|``gj~HS?bLnoh3Ga>rGqSRwg&jAZdczM^1Qkw;cZm$r&d|aX6rxfFFfEw>c_h|EzK5ql&(jeqbx8(6 zXyr&Df16x?w@kk*;+S`?f)G-WIqXqbeV`iew_lwu&)Cm(@ZQ3K(yr-qg?7pcb6myg z@14+0bHHUc0zSJ0s<+{iWGV6O@BYCLL0~c zSs255RwwH9?7RnDpd>(5@1Wm(=5hX-yoF3b*V`VM3@428J8aVh5JIcx;zT=V{qpCU zrF}O}P6L7*vg?5Fd<~iOY3WMZb0B5Gv{DBJsNfW?W>eI^rcAMy1Y2taEJd4rap@kf zRAm0%z>+s?)B}%HUo~WvaG-z0Rg6x1`iNWS082(R+mYsNCaU{Y)NhpVuV4XylEka! zw+?tR7(Hen=FKrw5lR))VpMde&D1IG7fq0Cr(pz}$}!Yw0o)x|z*jjV=0(|@#s5Zg z7@p)IH}rwTAA*Y^WFB2!ZZzv5d`fE#>iZSR8y@VuOz@?=;izuv=ReDQNG3U>eVQRX z1D*6@NEKjFxQhBCWZnV^NcP?QJ?*Gd47M>{P&z1TbdfsLm^z+@JQutR84n0Te8JUC^Ml{4X=JSQO987IQtvl+s@!v2@ zW`3(r)zf=_tYy2^wMGL!i`u?o{yf@pA(nA5Fz_b$g{iPA3O~+}xKf1GS%n?kjQJG3 z;?3waH+L&lU-zIHuXUD8{P{do zMzGXut_2``ybyMT$7G88Zpl8~&*%M;k}yGcRnZHQ$e)eQTF;Vo-G`m|*;PiIVKA^v z3Jw4jnycv;KaKzR zM((T)8(V`5K`-fbQ*08dT{|v?a;GXDmFt`-Pr4hO2H^nhh%uF8+JOOQmqDuhis-`EGEhGeBIT07u7XQ&e>7e z2kI`08V^?TX{nU8BaK$pN>k#&;2&q>smBJAkYH?&K+})8A85iC5O>A~!+L{sRYn{X zHw!4#f>A{+8h0i3jNzNF$k$XSQ5HzbTG*89hsj&ZZ7tY)?A5Q17vA8!cFipBmt$#vO zmKK5b=rF;*2BTRn7ncz4`!6%x(emZnfbbG@%GyB2iK zkAKYh167287~P^6@$9p1FuE=R6-65NF__%&c~Q3WQ(x}w-P1qw$1|xb+sE_ewaZ4r z19{w+HFtx;Pv+k14@k1?Bl4;`L{(F(k$mXC3VOO~->*2n>FoNu>U!Hc4@S@bGxfOj zw0$hbg>1&TX~JjBu;-})g26z)xQym@L;W(y+# zb~Ph)<_)>vRu@??XA-CYg>E5Y?csIYeH)8iYan4NNfbmM+y|Ktn|pJYZiMP_IQsp~lxy2ClTgNa1=6yjU*!HIV>N;o3Dpwd#yjF7ZX*nZou&F`bv??cB0yxYFK7pE{cE49imP&?~ z^1>4uAb&M(1ypCly4p}csUcHtrZqa?^sipX=A^eKD2 z`!)-ynOk=4GWk@_9k_SeT3!F)!lE^uH^M@9{-UQx#*hfH@U4o)ofJbb0oEXq#sFiI z!;-0+*f~Q6ZI!U=@6^M^tM&BskevX8$R$4%u%6lbR_!QOD`}YhXY$dK*JCPzTjFAK zasthJ1mC1mT@T2!&uFdI$d#O^eSe#7i`X4ae(55+3g-7~w~!VAx;Mg>lYU`&y@6zI+LBUq12%f+;c@eJRFSpXFfzac|1TTMS)hE_HUS5z+kg zy+wXoY7-Dx<7#nrb?)%J^zL6gMp**^H46cheMfm-ca>HXF5L!Gt~XyFWEdbs^8Ky$ zRMxt4b(QYw^ia7omvcU4ZLRv3^VJN0-igcf8+K^IPGDE-x#}gNI}Z;5ibX>k4xXL^ zKBwjT$2anZf3=Vc7P4uqHm` zOF74krBpgRgOnnNXlqDza4{=3n*H+9_^5k~5(crwyNBBpYdY*af6D+5lVnkQSPW#S z(fPqdNa*Ki-Mb1#ZslHF-G*{@{x&2p8q6!v6~JHg?*02+TyKbxq2XJzph=%vgKy4a z+3LT;ENU;_QV+%J9Z{Q7G1IVHs{jt5%FM8RJ=S$UhPY0Yy1@sX(%#3?+J2&a*j4nHM2af3=aAL9xqS7m zLCdtH@!stlL$Z0aH}SNQ7&ZU-y)^FoHGto@OC#34$lVU~p|PyI9Lck`p1c6QWq@i~ z(axV^OQy0^=s=8IxKS(wDCqy3Eeq!d;b|;3hyXxD{bDdnbyYP%ytP1mUmrOT%u4`y zcSH4^fwUaYvEcQ3up6Hl%6I}tk?)Sh`t3N9{MI5GxV?P^i7ItCc%QzW=CZajDcaKY zxkQYLiVBoD^=EDl2BnH+2Nsssg!u78))y9rMNf_o%l4_gJH>ZQxKjY0>`xDT>4vW4 zvyeZF^|ljEl2YR0;_ud#!1l{UUG6QFk-JDIgy`$MUho;paL&{Y!T<(?(Z3ldC#io` zWI_T4%LKpwo6HMdUR*>8P8Me_AQMGrA4@=OE_7Xi^H*AJEUUg| zd@1w1ZZ1DQKGr@N-dsJ_S;OJfkn+R;S2#eSd1gnl5F;+*h<5=_Y2@9Yi62LV?&{?Z zr4ai?foG!Vk{{SmKyR%!Ler5*SyR>Z!Wm1)$B$^R7jH0;kb)poV61%q@OlxcG)x>} zRIqdzcd9i2X&HWsDzJz`7=&vJ+-!Be+k-%meJdVtJ)-#1*0)xaV0lL5)5N2}cJrP~ zj%-_yhT#DD-@((?e8@bx@sSz&P$A*YUXfoWL_KpXoqebGjf9Q|hqZ(-MDIG8^@O?P*92-6?qOZ!pj*^A<$#*=R6 zae+Y_#sdBl^&v7Vj!4;&?Ic3WOchQ+?bk5WkWS8Z*4?JSn87avXs()C`gbL8aX=I7Ky&D)S z_x(4kpknXYy6vq2PM^X@vGC&2pKC}iyL6V!^|QwY2Tc8J9tM98d-^~Q!z zZ|-5WBk4zBr6mV}PzI6a3r4{Y2+TV%xYy%72BWdLIetjzmYOR`D30w^1N+sAkG_ii z-JmDG-)=sojUgAQ2X=capS?B`c$50xc!U%ew`6V3M!w(`DYYFJXB$&+UoKu5bzkH; zd-rNCC*^#rN~dT+%ch9qC3tadS%0pos>&*e8%F$^qukQZZ+RSv3_hpx{e*!-Gp-5HZ}#! zgAV6|9_AC@j?ptPp?nQp0+e-`h6~}SoZZ9kmE|cd;pk%)H~ppMuHrY`kMp_rExA5B zl#j!?*Dt01u9l2WoQ{=CJvu)PNZqVI#{Xtx;CpRxd9!}oB=zsRYmTVjj`?Fn&_k@N z&%>+TgY|2{_3Ml~$-kKz6bV-A7P2YvM9?D9h2_>B@AZ5f6RCjdUIye&^{iV-SCu2z z^d$~RWF^4G(c3;NM5NyWfQ-o}Ca}VPfeACpii@3%=b{j4I5OW!xf0P#ofo;ZT>v0> z8g&x{K@YcasIXrIl5CWy_Bpym%9+n*8K)_yTw!i;jbpm=w70@1>ou8MjM&=a9F_*K zgeBtM&;w3%1#Jxe;BU|g#i8J|*c4Q{Qsz*KX(NhjH@wAi1jllb)}}$jk*cO>_YTeD zVbI@Q!UXXw2IYFUR!;TY@lA^IDm+DNB^dmQp7}uUEs1! zp967(vu)M&k}-hkc>oNqc7Z=ccc<_DZyFnBDHPq_3ls@tSAtC^%IQOk?U77mwn3*; z3aGr%xx?wJ0mtCR+tqKMb_U6^I}Xyo?O*K#f-8ph{LnyF~c{6&+|*sifCTE-);r ziTnaOV*b7);@LE5j3g!ZOW9ACi?|4b6q|j^jyR`l-X35P3K9>0 z)2uD==cl{_nlw~J@AtpMKv1P;1kk>URC4w`(LA+X_B*z(5UR~o`>?A;s&J?zK@diNG&l=$ zaN5S0?``E^lE($4^BF`gu-WDKP*utVb{7IT5R_FlpRt0_k)6F zZyBocF-eXGD{%aq;phBM-Sl#6x9?sYcs^TltPjUIE%1YYZ*^-WgIC4Y=Xmmk)h=Ae zMN?s01LDA6HV=QMtJe2Y3&;zWJScs>wRB2_Je1>afqxR@Hr1W+XiH3z_Lh99%JY-W zo$5EU;a&tvEwK#DepyQp9M9q0vmy|CVjNDtnL*Zd$eV&7ljWsSj&WR?z)+W%z_@*Q ze#vWum~{=mBL^EmMFu%6H<{%4d3Ij(lLahNxv&P#EhQ^XS?S#grTfjLP;HW)tb7V$ zdWRGUQ~&|x%6^UvXa>qbT9xuhfMy2id3aHO!sJ3=-e}$6EZ5&xweG{%$<*D+leN5Z zVQmeA>0om_ND5^_{^a3y&UKv7@}zP&rUPcvD8W3XCOodVC)3#{6;Q^I*!*b*8ipSeKiaTIIZQD zxasF7(V$G=(E>ERp9{Vt$EF}82i!yC!-)1dc>A65>_xY60VJ>0bsc@u;?sQZC-rC^ zSK54))23ikD?Fc}D+eEk^(1UEXx}Bu&xH7WyJp-?X%JB4Ex(@}uRyP@dA4td|&0slA?qGifK#3-?lS=7aFcs$FM8&4^xDsj18O_i#@H1J^oyOM3!wO zx&|_#cRqEt(fFNvocWr4QmFbs$8MP7lonZ#c6fZ8Bh^APq+?Q;wNyt&Os0>+rF4nZ z++duR5Yq8WnI#uv08#NDF^Cdp;W`#PDlL)Y&JwQ10hcX~Pq$Bj-ZKJeX#E9DZ-ngN zuv#-BmevwSFnruHA-@t^NOuf@OB90&pnnXulg18V<@t(@ghW_sq)r#!y$a~deEOTY zc3!MgaY`EW+jhs(l%xn~P_C@k(gHfRj(jTud=7vIB<**2xN@K2`s4+yuC^GeGDk%F zEUn)yC`y54o4uT)(YXzmLqk|>bb-={C4zS_d1QaFp3VpqKHXe-APON&pujqmcXe`d!(uDC@i&|I#OfL2?+d{3TL9lyVkkOb-s2*jozEdi z)CBM}HQu(FA*28BWg!+(>ZRe0MwYD|jZiYxClrMiJ$|OLoc|4_K5F_$g>(F!6^e^Y z%o|u*GKzWYDW1HxiQKm`Z70J)Na#rSqGc!NHQPYJ53@x#1MYDSWrh9~=J@j{v11;> ziQe%z$y*GlKQh;X6T3eBj20aK{3CLLcm_0SoaRKX_;g;ZnMlwTp(E60a4$7KipHgH zRkG9Np}~)&s5`GbsDdfMh8!f4muczbAmTz;$k9hRZdu-XOdCZioYQx&t~$hy^@NV)LRM=Cdh{KN+wUQep~Q#vsog=xjQ-p;h@- zDTP~NtC_`=T8`=v{FyaU0Mb=a&Z3@0S5OcI;j6KQ>%}VcYr=>NmX}z;rRg6N5=Z(E z8VrqgMl4e4tTzE)8J1qLm5FCfQ9M|>Vw1MkJXno>@iQG-7& zjZr}2t3^abv^DBadJ z@&cDwj=J7#VP^n`1LG#%`JxdfB0rsS06wqhLIMH2EbmO zdVizPDo@f}vL{@@3P7|w!kw1~K~b7-4?*9`DayQj%ckuZ6QVTB9b)KYL!Gb<6U$rA zrWQSHqY9dR&X|K0)x3#|K_4;~fNne8YwbfpG^qc-5tj6uNip-|u$K_3SOwr~TU#`u zokX0^A$S<9%((My+;9}N!+%V*v$$nhbt;?$FyL|olz#B_So6IZG^k5gn`efTv_Ll- zC!hkkbBOEvpPs-=OX#-()_(tnZ^*F@^2*b9hTh`f^#x4yGAt<)48rZkLJ<Pf~B703d z(ZdW0^}+!9{Umv|BCGO^sANM!MttY4J}4ENQ)km@ z6b3)C3yvRY($~@0%3^s5i+TIX6v&U#MzTv?nvK1l1+qdxr~ z6jFa7s!_L~~P+9g#MH^e02CJdQ8rLTac}2XX>`W4eOjbmeT^Zx>HC zWuWepWu{10@HIR~msm?YVA(lj7l{h#j9sl?jW(J>3hZ9B`JhT`xK=VO(cU$>zM#L~ z2)d8+a6w*N$Hn$YD>w4}`b*a6wAFTI$X{Mwija&gil>vr$J_Io50VMbmq2Hg_%vTJ zllVM`W_FL?caD?kURcrIk%2q5I9+A>uezQd46PR|N|GLbWj_A0tQEPdt5=&jJUqmb zRT;^u1f_vNS~`KJO)Cz_HTb_#u4(BcbKCo=$BM)^Q}0mpOE$HV>%{HJQz70i=FY=~ z8We#oivrkbKVyUtu6Fo5G&?&%3oJ zSIL{jsd+VrB#rF#>zC^fmoU_?TOaWQ|D;ncv*m#BAPJ5bLSHS&25>beaAmM!G?6JX zGw_lYYW+)OIU#DZVS8!XOdw$%4?Hx6-OuQo&XdTn^8?}mrR)=7ord%)X>iygbp09M5_JJ;X*u7;|NcnR|2NJgSQE_*FCCJYtSl{YW{L(QqGc%RTwDo4C!8XUo zAsw#76t>q<)BL3{xI36SUJn|lC?>7nu`6CcHW;300}ldGyQra=U&z}$ z727vo$$1M{Tf7JbQPFv#S`00zV*?@0+{D+;KcMKE)TJV9dnhSx{enBXO|#Xe0S~={tcK}e-}kGrUBk3G2XN8$Ro7*A z4nf;r`gmQfBQbG<`H!<3RotI_aT%N~0<>LQKUjyN)fj@s`~m=+kO4mOG8#i4bcilL zMcOQCUSS?x@#4s;a)y!#YMEFVlmCQ_)3U4KkLGZCK2<yn~=Jj@KW# zXd#Z43WZ#jJ@~=HBYSHTs{?}kB}Ox~0)7Wf;23HtZ!%6aeQyzr;~B?Vmc}+FqG{f~ zEGtO9%dQv6E8q5@+f;x*6I_sy|I+q3S@()j`N3~V#UbB7&A1dV+u`xz@Rf=hcQ2l# zHZo}|FQZyq0w%ys_4_T)$MSM_TJob3y%dJIW)rTyr=hjBXO1amTW_X}PTIKFw^nY2 zBqSte_%osvC_=4yQA4@N`kA~2K+1UNa)`G~CQ%SC%SUJsU2y_?Vm}6q{t=wWhr-rR zX2s|fnw-Rl!@%$y&KRnYVSM#&>Yc{b-m%9+r|Zc>r(@NK#fzt#b_9|pB`rVu) zp|)CQYk>MRorJaAA(ij6Z~FWjTJYG6{RD0;m+c#`m)r?X<*ZSBp}h}iEALl2gumP7 ziZ=XdR4_ZS_EPfPZLM6iG~_Me{TGwC*fy&;`tDotF87GkN&RqNnZDlObA`$dlpal$ z6Fptj#XJsSg&%Z)fxQwIJ=)+nXF4IFA+Gry={`LO3qc*` z8ulLV*td6-J(yqq0WVA>N_cNxGsNMFCh=Ux#l$=Ki(710E7#(has7*@{$L`EFk{2z6oSDCF!bLn7me6X_$;CNw<=#HuZuo z9p5%FKRjky+Xx38HCst=fTQ*Bz>+9<7b z=^BolgvOZF)Wt>YUkpr2el3|LmRFX8^_|R<^CL;NFhi+kHYVmDr&=Bh827_{{J7c< zX*h8^ASQ_9;0i?<{l?5}bNxYgDVbk_sxJRRUT=SoufyxEZj^|lk-vZ_*Wg=_sK_b8 z{g_47VY!v#O@fXX8-sn0zRhxD;jgS01TfJ&9$9r6rtt4HT(ZCHKnCjeMtYUo9Io+B z85IT)oCwYHZ=8C9aXU{n2K3d=eSV-BbU3TmXYoV8X7?#e=#!-5_zpz&!fZ5)3@hO$8KJ_#60^=-Aa02`GPgcqHt}J z7m9*zkSW*e=7D~CR1?9;nwJJA$77{Iz{dn%gnVi#qz59U!QyO^1bi85!dayb%uE z5Pqtsy?Gq5K0A5*!yT|clY4W|KjoU``|CfWR3E82VQXfp+5Lt2(*>2K<2Y2lke#pF zfxczb<(CCdEnRjrg|U{0A=^_64S!PD2r3FJl8-%+9#f6!aCB?zxofrTJu+9eY|;PW z2Qm?J)%!%h^Y8gk;8R>`Q}x4b&0}BFE*C4k`>NdQk&k`F^w`Duij~BRO8v6LA28G{2vMb__m0_eER{&KaRu1WhS7Nq zp{%2I_vyJe>9H5L^|_DNGr3QR`mP+K4o62P?{~VMvQO~d)OrW(jB%Uol~y+~w>f|5 z@3)3q)5#SX{`eg|C6(BDFd{5*a~WN;HY|$b|9B|j`hGKiC+Fc`)kicLfl+j8OYPup zbR|4`feb6y=fc(HWlMA6W$_SwS7MtyB=!1$9NweXA6EZ!k#`>HrHwOshdEO4 z%zI+waWMCX?I(Ov^{-+@JijWQmQGFW8%EkVLdOUL?)ck%O!BUIs-!4^1+{ z!)UvzI+*wz6a5GI@U_baMVJ^HAGEr(m<$e|T?m3(-tB3}oc{OOoP_Rrx-Mzat}}z~ zN|qbLY7xkVYCFrc#^2d$b*Al=oy1*q7E8|yRclPYraC3H2A0f>VlXn6bSg)pqWo^+ zP0VTIaplrBa<6b+HfufqV_92TOdlQkVYKesw{!0?j)V!S;mYy@M`Py_)0y=2t`;x$ zgyk#($SS{t=X$`Esm~1zCgJl>k{dNrCDq#5sv}Evv~E1lxXH!T$5U)jgcB!v?&gg2 zShPVtINh}M9b1Kj+i%q1vz@6i-V1zOdkQY>DKm69Q2>NeuFjKacc^wVskB z1M>#sDz?e&#}M#tsNZ88x8no4WzV?Dydd4=+Yx8Q|6}SbyrTNUtv@tK4&5;zCDKw- z!jRH1l$69ECEXw`LrVw@Eh$4ONVn1qAR^r@AYIb;{O)@1TJIk)u-2J5=X{_2?ETr$ zz>%TF0GW~jboPWK7^oGI4wV-w@j9K{&p934mw_!V%h_&;IpMF)Zs)-!MJ^X!LaUnm z9Wax5!al$Jj;=B51iyBf$RuFT+{wQk%pcu8ZOQA?VdghP1^6{_$67Br1-;Z(tzAm+ z@q3yC9nA;nGT+#Xy(0XibKaxx<9h&h|DyzM;+O8G0~W%g)1`S?Y=ynnuhaD%-}e9$ zL6ZahmyulpPd>$?dv_FX`DQ}|jc$01Dx%GlF90nVuK_!;uGbR};p80_?xXDn_Srqj zO+?A#NeYdQ(ir`W;?q+xg*-z2lc;CJJQci*RdVG{BH3eQdG9IIG%KczH;SNxUPT-p zeMHUEz321Ot<|md!R*^XyJs@cGy}AAy=kV2x{9ff%Y2rjIgBlm;uV8X-`>19V4tDf zU0qEYM^y?K%&>FKD` zsVl`^I()L^U~K@awZQ;rXm7T@vlNlm@efzeTW*iP<6BpUq&}NtKo4WD$Cj}{iM~Hk6vUr1z2`h z?g7CXpHKP*7yUm-VQEwe`)K*%c}5-w)cE+`KN zB#WM(cXV`&9(kC)$$EUv>$2j1WK$g^jI^ya_DHg%WUDcPP6#QeP1QoWbTHSAux77vD-3Q(x{a)r_S?n(f~G$!US4<(C8~_8HK19(7JOv-4(LdG*Z&HTa{c|Ug#O) zSKA=%jO7k*!Y(i2*IPL$56=GyDS%mt2YmM@>HdxSj*E5&3`%n0i*Y+{|1bl15Z$|i z8kixH8@fHz6`Uo1Pr2$BE5sX=es!ivm#dJ6W91n`052 zsdFr+h&QsiAnxIC1g(nj+X|mB>eib;&VbchoMisA$6&j`M^6>XQ4Cz-O3>b(g|BH@ zzsI|NKCUGTpc~UK(*gyZ1K@}rNHi@DV~{IMCS?yQFuIlfYCuwaI0~gT9E;F<+p1+< zV0<)nT~51RL@HRPNRd1_LZ#NeE|G*ValF5zDrq5|{a$=ODMXmo>2} z(8$(DGrseSK8=emzVn^EE|s|wwzhsMp>l0kso8S7EGo38(4h{1;j+498$R+(Ye`UmS zv;DAW_HeO$U7oOWZR2{EKHYx)3It{K)zmFM*s7Sq=S$&bD{>+L^$MC4eL2h6Gd|&i ze0~Ar7YE3D3Mf5n;s2aPWK9^3{lKd{_S9*RRp@SO|z@bDg1SZ;TX zELxUKa$hM)?i+*~e=-oP(={yryx8o&&(3n!ei-<0nb)z6&geKRNj(i*y4@0gI-d(1 z$CGMW7=afwF;p2F!d;;@uSDi<@K*DzufnL6S-Cy3BvVR?*!w`<}G56tinr}G0T42&NH{$5aY+;+lf(LBn%+xkdRmF!{Ga7}5M)iNCVE`_xijl@!(U8&CT)Q%Cty6C!xqA6 z+Bbw4rUOe6Vcp4EKBMS_Wo>+%OOK^`1R#b2Yj6Onceu}VHcO^z4pSI#c?Cf3d|XJr z+ETqV9ks8hOdTxVlbV!=O4^@&R*Mfx6P(eF2TfvN4NXuS?7jO z212)wpZ>(9AdxLaXsQ%bpWh;;(?${-wBf1`BF;xMjFw4Ck+^4&Pz{3f;3)!B zIz?Jgj`|DoD{PukPf9DSdx}k$o)o6h6qyv~olD)9sk;CBqV0z{pn(;>U691o-M8Yf1(#HrTJSpi%I`%AN!hz~g=4__x0LZQF)mHR%f z6)G25cI6s>SglI1BG>lynoo@Us(1W>04_ z6$qh5aXXGwn<%-l*S^93c>$UWe%h1wgB!wpwutIJ<@~J6`ksuZ__Dtkhe;V&PD3}G zK^_NYDukg(KZ2gR&yYFq+zFJk)zN^}y1Po5iOw){@1owo4H7{)%btIV1ri}wf_th{ zWAO4S7eXXi=n9+oONeu%Vb4l*6=HFd!rrB$4)J}Px>@p5*KIE_PW?9-667S$EF-ZN zhy-f|m!2_+m>#Jh+=XX^)X=Z}FX|RsncDYN4^e)7yzNZ7a@xoQ1aaTvXsxJtL1zOZ%+w5}ege)cJ$1HFMOB{T zczws1gx{EH8k)k%Y7Mka1y<&~$j9Db!9kqla-CguPLrq2@funB6j&Y;LO*D{!3y(y z(Qh1pumS{PP|GN_E#c?I{NuN7!}e&`Gugt+^Xar8r-^PQc-kYcbaGHsc%g%(*rx}) zsaD_lht7G4+z#ceMemeOK*I^ad&Nj(7x?r~_te{<73sGu&r^yY7T~w^xTPqCO{>c- z046i*T*Jgq|A=T~F80;5B|s#8xbkf~WzZhj6_>kR{g~Cq^J&ZA*V!k-0?THi zxdp7@b|#A*@Ai9Vss*a|tF5?O#U7SaLK?YIhxZqqe#<+}RLvW0LwEwRmuT0ABFnqq z{c;_hX%aH+fla4!4|kR=_qod_2fo4jlKEQ0kLWMnz75$b*S?#>dsywY&o2?weVpFb z5`0s#Z7JJxZggpABA@bv#E$OL$M-%u|Dl!TVKM3coaH(xFKB`SI7zA<4Gm{JtWb1b zCdr*WiOYS3f4*Ayl=BqrU*8#g{LAwdtmAgKijzsEARVLml}o3`)stHD0T18iZ^jps zrJr!P<6o5StV~r1$lUy$)w^iC%!X0`(buVxeY4gR%iEFs+p&;a*S7URF0Hp(*>;{+ zz1uYwi`B6o#(iQ#E}nOu?f`D&=#Zo6rSocE=iJOHP1KJcc6A|pJ94-G=}wzTikk>Q8rpO*Hb1c>2SvzoUcq>eB8EN(5l$qyD?Y!q+S}PtK&%GYT4oC zUWT5wT$I!R@56r*nxEcI4z1iTwjLVMX&_iM-6jtm2#y0 z7R83ZQvOPpSm7&u)flmtyF6R92Beewt;5^I!_ncOtD%zR`=f`Bx;h&JeLg;Mw-2#g zk)W4XTO-*(QFgX5B`}?BhTNz;{CnQHH~$|b`)S7Ta@$uKq+ug