From ec6ea3b80817bd732556a0b2fff0085711d37d7d Mon Sep 17 00:00:00 2001 From: Raffi1202 Date: Wed, 9 Sep 2026 19:30:13 +0200 Subject: [PATCH 1/7] Support custom Tramp power levels and AUX pit mode --- docs/Settings.md | 50 +++++++ docs/VTx.md | 50 +++++++ src/main/fc/fc_msp.c | 4 +- src/main/fc/fc_msp_box.c | 8 + src/main/fc/rc_modes.h | 1 + src/main/fc/settings.yaml | 30 ++++ src/main/io/vtx.c | 33 ++--- src/main/io/vtx.h | 1 + src/main/io/vtx_tramp.c | 56 ++++++- src/test/unit/CMakeLists.txt | 6 + src/test/unit/vtx_pit_mode_unittest.cc | 78 ++++++++++ src/test/unit/vtx_tramp_unittest.cc | 197 +++++++++++++++++++++++++ 12 files changed, 486 insertions(+), 28 deletions(-) create mode 100644 src/test/unit/vtx_pit_mode_unittest.cc create mode 100644 src/test/unit/vtx_tramp_unittest.cc diff --git a/docs/Settings.md b/docs/Settings.md index 0a4792476b7..f080794f07a 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -7580,6 +7580,56 @@ Enable the 3x shorter stopbit on softserial. Need for some IRC Tramp VTXes. --- +### vtx_tramp_power_a + +Custom Tramp power level 1 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 10000 | + +--- + +### vtx_tramp_power_b + +Custom Tramp power level 2 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 10000 | + +--- + +### vtx_tramp_power_c + +Custom Tramp power level 3 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 10000 | + +--- + +### vtx_tramp_power_d + +Custom Tramp power level 4 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 10000 | + +--- + +### vtx_tramp_power_e + +Custom Tramp power level 5 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power. + +| Default | Min | Max | +| --- | --- | --- | +| 0 | 0 | 10000 | + +--- + ### yaw_deadband These are values (in us) by how much RC input can be different before it's considered valid. For transmitters with jitter on outputs, this value can be increased. Defaults are zero, but can be increased up to 10 or so if rc inputs twitch while idle. diff --git a/docs/VTx.md b/docs/VTx.md index 523fe83ae98..29110943f25 100644 --- a/docs/VTx.md +++ b/docs/VTx.md @@ -31,3 +31,53 @@ If you have problems getting SmartAudio working. There are a couple of CLI param - If you are using softserial, you can try using the alternate method by setting [`vtx_smartaudio_alternate_softserial_method`](https://github.com/iNavFlight/inav/blob/master/docs/Settings.md#vtx_smartaudio_alternate_softserial_method) to OFF. - If you are using TBS Sixty9 VTX you may consider to set count of stop bits to 1, using [`set vtx_smartaudio_stopbits = 1`](https://github.com/iNavFlight/inav/blob/master/docs/Settings.md#vtx_smartaudio_stopbits) + +### Custom IRC Tramp power levels + +Tramp normally selects a built-in power table from the maximum power reported by +its device. For devices with different levels, configure up to five ascending +power values in milliwatts with `vtx_tramp_power_a` through `vtx_tramp_power_e`. +Use consecutive entries followed by zeros. All zeros retain automatic selection; +an invalid table (gaps, duplicates, descending values) also falls back to automatic +selection. Reboot after changing the table. + +For a BLITZ Whoop 2.5W: + +``` +set vtx_tramp_power_a = 25 +set vtx_tramp_power_b = 400 +set vtx_tramp_power_c = 1000 +set vtx_tramp_power_d = 2500 +set vtx_tramp_power_e = 0 +set vtx_power = 1 +save +``` + +This changes the power values and their labels, not the VTX's reported maximum. +The driver continues to clamp requests to that maximum. Only if the hardware's +maximum is independently confirmed and the device reports it incorrectly, use +`vtx_max_power_override` to provide the correct maximum. It does not unlock the +VTX or verify actual RF power. Power selection remains one-based: this example +maps levels 1–4 to 25, 400, 1000 and 2500 mW. + +The VTX settings parameter-group version changes from 2 to 3. Save `diff all` +before upgrading and restore the VTX settings afterwards; the new table defaults +to automatic selection. + +### Tramp pit mode on an AUX switch + +Assign **VTX PIT MODE** in Modes (permanent mode ID 69). For example, with the +first two mode-condition slots already in use, this assigns AUX5 / channel 9: + +``` +aux 2 69 4 1800 2100 +save +``` + +The assigned range requests pit mode while disarmed. Arming exits pit mode and +prevents entering it in flight. The switch is ignored without a valid receiver +signal. Without a mode assignment, existing hardware-button/MSP control is left +alone. Pit mode changes use the Tramp `I` command (0 = enter, 1 = exit), and the +driver retries if subsequent status reports do not match the request. Verify the +VTX's own pit indicator before relying on it; driver support is not a guarantee +that every Tramp-compatible device implements the command. diff --git a/src/main/fc/fc_msp.c b/src/main/fc/fc_msp.c index 79ee6da48bb..c68a840e216 100644 --- a/src/main/fc/fc_msp.c +++ b/src/main/fc/fc_msp.c @@ -5072,7 +5072,9 @@ bool mspFCProcessInOutCommand(uint16_t cmdMSP, sbuf_t *dst, sbuf_t *src, mspResu sbufWriteU8(dst, powerLevel); sbufWriteU16(dst, 0); - const char *str = vtxDevice->capability.powerNames[powerLevel - 1]; + // Tramp reserves label zero for the unselected "---" entry. + const uint8_t nameIndex = vtxCommonGetDeviceType(vtxDevice) == VTXDEV_TRAMP ? powerLevel : powerLevel - 1; + const char *str = vtxDevice->capability.powerNames[nameIndex]; const uint32_t str_len = strnlen(str, 5); // these _should_ all be null-terminated sbufWriteU8(dst, str_len); for (uint32_t i = 0; i < str_len; i++) diff --git a/src/main/fc/fc_msp_box.c b/src/main/fc/fc_msp_box.c index 0be7382ad07..3b0070e779e 100644 --- a/src/main/fc/fc_msp_box.c +++ b/src/main/fc/fc_msp_box.c @@ -117,6 +117,7 @@ static const box_t boxes[CHECKBOX_ITEM_COUNT + 1] = { { .boxId = BOXAUTOSPEED, .boxName = "AUTO SPEED", .permanentId = 69 }, { .boxId = BOXTERRAINAGLHOLD, .boxName = "TERRAIN AGL HOLD", .permanentId = 70 }, { .boxId = BOXINFLIGHTMENU, .boxName = "IN FLIGHT MENU", .permanentId = 71 }, + { .boxId = BOXVTXPITMODE, .boxName = "VTX PIT MODE", .permanentId = 72 }, { .boxId = CHECKBOX_ITEM_COUNT, .boxName = NULL, .permanentId = 0xFF } }; @@ -337,6 +338,12 @@ void initActiveBoxIds(void) ADD_ACTIVE_BOX(BOXFAILSAFE); +#ifdef USE_VTX_CONTROL + if (feature(FEATURE_VTX)) { + ADD_ACTIVE_BOX(BOXVTXPITMODE); + } +#endif + #if defined(USE_RCDEVICE) || defined(USE_MSP_DISPLAYPORT) ADD_ACTIVE_BOX(BOXCAMERA1); ADD_ACTIVE_BOX(BOXCAMERA2); @@ -448,6 +455,7 @@ void packBoxModeFlags(boxBitmask_t * mspBoxModeFlags) CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXUSER2)), BOXUSER2); CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXUSER3)), BOXUSER3); CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXUSER4)), BOXUSER4); + CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXVTXPITMODE)), BOXVTXPITMODE); CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXLOITERDIRCHN)), BOXLOITERDIRCHN); #if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE) CHECK_ACTIVE_BOX(IS_ENABLED(IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE)), BOXMSPRCOVERRIDE); diff --git a/src/main/fc/rc_modes.h b/src/main/fc/rc_modes.h index 59de340bac9..744e2c2734c 100644 --- a/src/main/fc/rc_modes.h +++ b/src/main/fc/rc_modes.h @@ -88,6 +88,7 @@ typedef enum { BOXAUTOSPEED = 60, BOXTERRAINAGLHOLD = 61, BOXINFLIGHTMENU = 62, + BOXVTXPITMODE = 63, CHECKBOX_ITEM_COUNT } boxId_e; diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index 78d8f20ca90..46c288622cf 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -4392,6 +4392,36 @@ groups: field: maxPowerOverride min: 0 max: 10000 + - name: vtx_tramp_power_a + description: "Custom Tramp power level 1 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power." + field: trampPowerLevels[0] + min: 0 + max: 10000 + default_value: 0 + - name: vtx_tramp_power_b + description: "Custom Tramp power level 2 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power." + field: trampPowerLevels[1] + min: 0 + max: 10000 + default_value: 0 + - name: vtx_tramp_power_c + description: "Custom Tramp power level 3 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power." + field: trampPowerLevels[2] + min: 0 + max: 10000 + default_value: 0 + - name: vtx_tramp_power_d + description: "Custom Tramp power level 4 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power." + field: trampPowerLevels[3] + min: 0 + max: 10000 + default_value: 0 + - name: vtx_tramp_power_e + description: "Custom Tramp power level 5 in mW. Set consecutive ascending levels, then zeros; all zero keeps automatic tables. Reboot after changes. The reported maximum (or vtx_max_power_override) still limits power." + field: trampPowerLevels[4] + min: 0 + max: 10000 + default_value: 0 - name: vtx_frequency_group field: frequencyGroup description: "VTx Frequency group to use. Frequency groups: FREQUENCYGROUP_5G8: 5.8GHz, FREQUENCYGROUP_2G4: 2.4GHz, FREQUENCYGROUP_1G3: 1.3GHz." diff --git a/src/main/io/vtx.c b/src/main/io/vtx.c index 495554e93ae..c9ecb9a9aeb 100644 --- a/src/main/io/vtx.c +++ b/src/main/io/vtx.c @@ -40,12 +40,13 @@ #include "fc/settings.h" #include "flight/failsafe.h" +#include "rx/rx.h" #include "io/vtx.h" #include "io/vtx_string.h" #include "io/vtx_control.h" -PG_REGISTER_WITH_RESET_TEMPLATE(vtxSettingsConfig_t, vtxSettingsConfig, PG_VTX_SETTINGS_CONFIG, 2); +PG_REGISTER_WITH_RESET_TEMPLATE(vtxSettingsConfig_t, vtxSettingsConfig, PG_VTX_SETTINGS_CONFIG, 3); PG_RESET_TEMPLATE(vtxSettingsConfig_t, vtxSettingsConfig, .band = SETTING_VTX_BAND_DEFAULT, @@ -130,31 +131,21 @@ static bool vtxProcessPitMode(vtxDevice_t *vtxDevice, const vtxSettingsConfig_t { UNUSED(runtimeSettings); - uint8_t pitOnOff; - - bool currPmSwitchState = false; - static bool prevPmSwitchState = false; + // Leave button/MSP control alone unless the pilot assigned this mode. + if (!isModeActivationConditionPresent(BOXVTXPITMODE) || !rxIsReceivingSignal()) { + return false; + } + uint8_t pitOnOff; if (!vtxCommonGetPitMode(vtxDevice, &pitOnOff)) { return false; } - if (currPmSwitchState != prevPmSwitchState) { - prevPmSwitchState = currPmSwitchState; - - if (currPmSwitchState) { - if (0) { - if (!pitOnOff) { - vtxCommonSetPitMode(vtxDevice, true); - return true; - } - } - } else { - if (pitOnOff) { - vtxCommonSetPitMode(vtxDevice, false); - return true; - } - } + // Never enter pit mode in flight; arming also exits a ground pit mode. + const bool requestedPitMode = IS_RC_MODE_ACTIVE(BOXVTXPITMODE) && !ARMING_FLAG(ARMED); + if ((pitOnOff != 0) != requestedPitMode) { + vtxCommonSetPitMode(vtxDevice, requestedPitMode); + return true; } return false; diff --git a/src/main/io/vtx.h b/src/main/io/vtx.h index 1a0fcaac48a..2a187e13a2a 100644 --- a/src/main/io/vtx.h +++ b/src/main/io/vtx.h @@ -41,6 +41,7 @@ typedef struct vtxSettingsConfig_s { uint16_t pitModeChan; // sets out-of-range pitmode frequency uint8_t lowPowerDisarm; // min power while disarmed, from vtxLowerPowerDisarm_e uint16_t maxPowerOverride; // for VTX drivers that are polling VTX capabilities - override what VTX is reporting + uint16_t trampPowerLevels[5]; // optional ascending mW levels; trailing zeros end the table uint8_t frequencyGroup; // Frequencies being used, i.e. 5.8, 2.4, or 1.3 GHz } vtxSettingsConfig_t; diff --git a/src/main/io/vtx_tramp.c b/src/main/io/vtx_tramp.c index 540c9c9f222..454742b9d30 100644 --- a/src/main/io/vtx_tramp.c +++ b/src/main/io/vtx_tramp.c @@ -24,6 +24,7 @@ #include #include #include +#include #include "platform.h" @@ -92,6 +93,8 @@ typedef struct { // Actual settings to send to the VTX unsigned freq; unsigned power; + bool pitMode; + bool pitModeRequested; // do not override hardware-button state before a request } request; // Actual VTX state: updated from actual VTX @@ -256,7 +259,8 @@ static vtxProtoResponseType_e vtxProtoProcessResponse(void) static void vtxProtoSetPitMode(uint16_t mode) { - vtxProtoSend(0x73, mode); + // IRC Tramp 'I': 0 enters pit mode, 1 exits (same polarity as Betaflight). + vtxProtoSend('I', mode ? 0 : 1); } static void vtxProtoSetPower(uint16_t power) @@ -316,9 +320,8 @@ static void impl_Process(vtxDevice_t *vtxDevice, timeUs_t currentTimeUs) if (vtxState.updateReqMask != VTX_UPDATE_REQ_NONE) { // Updates pending. Send an appropriate command if (vtxState.updateReqMask & VTX_UPDATE_REQ_PITMODE) { - // Only disabling PIT mode supported vtxState.updateReqMask &= ~VTX_UPDATE_REQ_PITMODE; - vtxProtoSetPitMode(0); + vtxProtoSetPitMode(vtxState.request.pitMode); vtxProtoSetState(VTX_STATE_QUERY_DELAY); } else if (vtxState.updateReqMask & VTX_UPDATE_REQ_FREQUENCY) { @@ -367,6 +370,10 @@ static void impl_Process(vtxDevice_t *vtxDevice, timeUs_t currentTimeUs) vtxState.updateReqMask |= VTX_UPDATE_REQ_POWER; } + if (vtxState.request.pitModeRequested && vtxState.state.pitMode != vtxState.request.pitMode) { + vtxState.updateReqMask |= VTX_UPDATE_REQ_PITMODE; + } + // We got the status response - proceed to IDLE vtxProtoSetState(VTX_STATE_IDLE); } @@ -446,9 +453,9 @@ static void impl_SetPitMode(vtxDevice_t *vtxDevice, uint8_t onoff) { UNUSED(vtxDevice); - if (onoff == 0) { - vtxState.updateReqMask |= VTX_UPDATE_REQ_PITMODE; - } + vtxState.request.pitMode = onoff != 0; + vtxState.request.pitModeRequested = true; + vtxState.updateReqMask |= VTX_UPDATE_REQ_PITMODE; } static bool impl_GetBandAndChannel(const vtxDevice_t *vtxDevice, uint8_t *pBand, uint8_t *pChannel) @@ -578,6 +585,41 @@ const char * const trampPowerNames_1G3_800[VTX_TRAMP_1G3_MAX_POWER_COUNT + 1] = const uint16_t trampPowerTable_1G3_2000[VTX_TRAMP_1G3_MAX_POWER_COUNT] = { 25, 200, 2000 }; const char * const trampPowerNames_1G3_2000[VTX_TRAMP_1G3_MAX_POWER_COUNT + 1] = { "---", "25 ", "200", "2000" }; +static char customPowerNames[VTX_TRAMP_5G8_MAX_POWER_COUNT][6]; +static char *customPowerNamePointers[VTX_TRAMP_5G8_MAX_POWER_COUNT + 1]; + +static bool vtxProtoUseCustomPowerTable(void) +{ + const uint16_t *levels = vtxSettingsConfig()->trampPowerLevels; + unsigned count = 0; + bool ended = false; + for (unsigned i = 0; i < VTX_TRAMP_5G8_MAX_POWER_COUNT; i++) { + if (levels[i] == 0) { + ended = true; + } else { + // Reject holes, duplicate/decreasing powers and out-of-range EEPROM values. + if (ended || levels[i] > 10000 || (i > 0 && levels[i] <= levels[i - 1])) { + return false; + } + count++; + } + } + if (!count) { + return false; + } + + customPowerNamePointers[0] = "---"; + for (unsigned i = 0; i < count; i++) { + snprintf(customPowerNames[i], sizeof(customPowerNames[i]), "%u", (unsigned)levels[i]); + customPowerNamePointers[i + 1] = customPowerNames[i]; + } + vtxState.metadata.powerTablePtr = levels; + vtxState.metadata.powerTableCount = count; + impl_vtxDevice.capability.powerCount = count; + impl_vtxDevice.capability.powerNames = customPowerNamePointers; + return true; +} + static void vtxProtoUpdatePowerMetadata(uint16_t maxPower) { switch (vtxSettingsConfig()->frequencyGroup) { @@ -644,10 +686,12 @@ static void vtxProtoUpdatePowerMetadata(uint16_t maxPower) } break; } + vtxProtoUseCustomPowerTable(); } bool vtxTrampInit(void) { + memset(&vtxState, 0, sizeof(vtxState)); serialPortConfig_t *portConfig = findSerialPortConfig(FUNCTION_VTX_TRAMP); if (portConfig) { diff --git a/src/test/unit/CMakeLists.txt b/src/test/unit/CMakeLists.txt index df023f02eb4..8fc45f2e50c 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -174,6 +174,12 @@ set_property(SOURCE mavlink_unittest.cc PROPERTY definitions USE_TELEMETRY USE_T set_property(SOURCE mavlink_unittest.cc PROPERTY extra_includes "../../lib/main/MAVLink") +set_property(SOURCE vtx_pit_mode_unittest.cc PROPERTY definitions USE_VTX_CONTROL) +set_property(SOURCE vtx_pit_mode_unittest.cc PROPERTY depends "io/vtx.c") + +set_property(SOURCE vtx_tramp_unittest.cc PROPERTY definitions USE_VTX_CONTROL USE_VTX_TRAMP) +set_property(SOURCE vtx_tramp_unittest.cc PROPERTY depends "io/vtx_tramp.c" "io/vtx_string.c" "common/crc.c" "common/streambuf.c") + function(unit_test src) get_filename_component(basename ${src} NAME) string(REPLACE ".cc" "" name ${basename} ) diff --git a/src/test/unit/vtx_pit_mode_unittest.cc b/src/test/unit/vtx_pit_mode_unittest.cc new file mode 100644 index 00000000000..5d7aa2b55f5 --- /dev/null +++ b/src/test/unit/vtx_pit_mode_unittest.cc @@ -0,0 +1,78 @@ +/* + * This file is part of Cleanflight and Betaflight. + * + * Cleanflight and Betaflight are free software. You can redistribute + * this software and/or modify this software under the terms of the + * GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Cleanflight and Betaflight are distributed in the hope that they + * will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software. + * + * If not, see . + */ + +#include "gtest/gtest.h" +extern "C" { +#include "platform.h" +#include "drivers/vtx_common.h" +#include "fc/runtime_config.h" +#include "fc/rc_modes.h" +#include "io/vtx.h" +uint32_t armingFlags; +uint8_t cliMode; +} +static bool assigned, receiving, modeActive, pit; +static int requests; +static vtxDevice_t device{}; +extern "C" { +bool isModeActivationConditionPresent(boxId_e id) { return id == BOXVTXPITMODE && assigned; } +bool IS_RC_MODE_ACTIVE(boxId_e id) { return id == BOXVTXPITMODE && modeActive; } +bool rxIsReceivingSignal(void) { return receiving; } +bool failsafeIsActive(void) { return false; } +void vtxControlInputPoll(void) {} +vtxDevice_t *vtxCommonDevice(void) { return &device; } +bool vtxCommonGetPitMode(vtxDevice_t *, uint8_t *value) { *value = pit; return true; } +void vtxCommonSetPitMode(vtxDevice_t *, uint8_t value) { pit = value; ++requests; } +bool vtxCommonGetPowerIndex(vtxDevice_t *, uint8_t *) { return false; } +void vtxCommonSetPowerByIndex(vtxDevice_t *, uint8_t) {} +bool vtxCommonGetBandAndChannel(vtxDevice_t *, uint8_t *, uint8_t *) { return false; } +void vtxCommonSetBandAndChannel(vtxDevice_t *, uint8_t, uint8_t) {} +void vtxCommonProcess(vtxDevice_t *, timeUs_t) {} +} +class PitModeTest : public ::testing::Test { +protected: + void SetUp() override { + assigned = true; receiving = true; modeActive = false; pit = false; + requests = 0; armingFlags = 0; cliMode = 0; + } + void update() { for (int i=0; i<3; ++i) vtxUpdate(0); } +}; +TEST_F(PitModeTest, UnassignedModePreservesHardwarePitMode) { + assigned=false; pit=true; update(); EXPECT_TRUE(pit); EXPECT_EQ(0,requests); +} +TEST_F(PitModeTest, AssignedSwitchControlsBothDirections) { + modeActive=true; update(); EXPECT_TRUE(pit); EXPECT_EQ(1,requests); + modeActive=false; update(); EXPECT_FALSE(pit); EXPECT_EQ(2,requests); +} +TEST_F(PitModeTest, ArmingExitsPitAndPreventsReentry) { + modeActive=true; update(); ASSERT_TRUE(pit); + armingFlags=ARMED; update(); EXPECT_FALSE(pit); + update(); EXPECT_FALSE(pit); EXPECT_EQ(2,requests); +} +TEST_F(PitModeTest, ReceiverLossDoesNotActOnStaleSwitch) { + receiving=false; modeActive=true; update(); EXPECT_FALSE(pit); EXPECT_EQ(0,requests); +} +TEST_F(PitModeTest, NoCommandWhenReportedStateMatchesSwitch) { + update(); EXPECT_EQ(0,requests); + modeActive=true; pit=true; update(); EXPECT_EQ(0,requests); +} +TEST_F(PitModeTest, CliDoesNotSendPitCommands) { + cliMode=1; modeActive=true; update(); EXPECT_FALSE(pit); EXPECT_EQ(0,requests); +} diff --git a/src/test/unit/vtx_tramp_unittest.cc b/src/test/unit/vtx_tramp_unittest.cc new file mode 100644 index 00000000000..32f28ea556e --- /dev/null +++ b/src/test/unit/vtx_tramp_unittest.cc @@ -0,0 +1,197 @@ +/* + * This file is part of Cleanflight and Betaflight. + * + * Cleanflight and Betaflight are free software. You can redistribute + * this software and/or modify this software under the terms of the + * GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * Cleanflight and Betaflight are distributed in the hope that they + * will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this software. + * + * If not, see . + */ + +#include +#include +#include +#include +#include "gtest/gtest.h" + +extern "C" { +#include "platform.h" +#include "common/crc.h" +#include "drivers/time.h" +#include "drivers/vtx_common.h" +#include "io/serial.h" +#include "io/vtx.h" +#include "io/vtx_control.h" +#include "io/vtx_tramp.h" +vtxSettingsConfig_t vtxSettingsConfig_System; +vtxConfig_t vtxConfig_System; +} + +static vtxDevice_t *device; +static serialPort_t port; +static serialPortConfig_t portConfig; +static std::deque received; +static std::vector> sent; +static timeMs_t now; +static uint16_t reportedMax, actualPower, actualFrequency; +static bool actualPit, ignoreNextPit, corruptNextStatus; + +static void respond(char command, uint16_t a, uint16_t b, uint16_t c) +{ + std::array frame{}; + frame[0] = 15; frame[1] = command; + frame[2] = a; frame[3] = a >> 8; + frame[4] = b; frame[5] = b >> 8; + frame[6] = c; frame[7] = c >> 8; + if (command == 'v') frame[7] = actualPit; + frame[14] = crc8_sum_update(0, frame.data() + 1, 13); + if (command == 'v' && corruptNextStatus) { + frame[14] ^= 1; + corruptNextStatus = false; + } + received.insert(received.end(), frame.begin(), frame.end()); +} + +extern "C" { +timeMs_t millis(void) { return now; } +void vtxCommonSetDevice(vtxDevice_t *d) { device = d; } +serialPortConfig_t *findSerialPortConfig(serialPortFunction_e) { return &portConfig; } +serialPort_t *openSerialPort(serialPortIdentifier_e, serialPortFunction_e, + serialReceiveCallbackPtr, void *, uint32_t, portMode_t, portOptions_t) { return &port; } +uint32_t serialRxBytesWaiting(const serialPort_t *) { return received.size(); } +uint8_t serialRead(serialPort_t *) { uint8_t b = received.front(); received.pop_front(); return b; } +void serialWriteBuf(serialPort_t *, const uint8_t *data, int size) +{ + EXPECT_EQ(16, size); + if (size != 16) return; + std::array frame; + std::memcpy(frame.data(), data, size); + EXPECT_EQ(crc8_sum_update(0, data + 1, 13), data[14]); + EXPECT_EQ(0, data[15]); + sent.push_back(frame); + uint16_t value = data[2] | (data[3] << 8); + switch (data[1]) { + case 'r': respond('r', 5000, 5999, reportedMax); break; + case 'v': respond('v', actualFrequency, actualPower, 0); break; + case 'F': actualFrequency = value; break; + case 'P': actualPower = value; break; + case 'I': + if (ignoreNextPit) ignoreNextPit = false; + else actualPit = value == 0; + break; + } +} +} + +class TrampTest : public ::testing::Test { +protected: + void SetUp() override { + std::memset(&vtxSettingsConfig_System, 0, sizeof(vtxSettingsConfig_System)); + std::memset(&vtxConfig_System, 0, sizeof(vtxConfig_System)); + now = 0; reportedMax = 2500; actualPower = 25; actualFrequency = 5732; + actualPit = false; ignoreNextPit = false; corruptNextStatus = false; + received.clear(); sent.clear(); device = nullptr; + } + void tick(int n = 1) { + for (int i = 0; i < n; ++i) { now += 250; device->vTable->process(device, now * 1000); } + } + void start() { + ASSERT_TRUE(vtxTrampInit()); + tick(3); // reset, capability request, capability reply + ASSERT_TRUE(device->vTable->isReady(device)); + device->vTable->setBandAndChannel(device, 5, 3); + device->vTable->setPowerByIndex(device, 1); + tick(24); + sent.clear(); + } + void custom() { + const uint16_t powers[] = {25, 400, 1000, 2500, 0}; + std::memcpy(vtxSettingsConfig_System.trampPowerLevels, powers, sizeof(powers)); + } + int commands(char command, int value = -1) { + int n = 0; + for (auto &f : sent) if (f[1] == command && (value < 0 || (f[2] | (f[3] << 8)) == value)) ++n; + return n; + } +}; + +TEST_F(TrampTest, DefaultTableRemainsAutomatic) { + start(); EXPECT_EQ(5, device->capability.powerCount); + EXPECT_STREQ("800", device->capability.powerNames[5]); +} +TEST_F(TrampTest, CustomTableUsesAllFourBlitzPowers) { + custom(); start(); ASSERT_EQ(4, device->capability.powerCount); + const char *labels[] = {"25", "400", "1000", "2500"}; + const int powers[] = {25, 400, 1000, 2500}; + for (int i = 0; i < 4; i++) { + EXPECT_STREQ(labels[i], device->capability.powerNames[i + 1]); + device->vTable->setPowerByIndex(device, i + 1); tick(20); + EXPECT_EQ(powers[i], actualPower); + } +} +TEST_F(TrampTest, CustomPowerStillRespectsReportedLimit) { + custom(); reportedMax = 400; start(); + device->vTable->setPowerByIndex(device, 4); tick(20); EXPECT_EQ(400, actualPower); +} +TEST_F(TrampTest, ExplicitOverrideAllowsConfiguredMaximum) { + custom(); reportedMax = 400; vtxSettingsConfig_System.maxPowerOverride = 2500; start(); + device->vTable->setPowerByIndex(device, 4); tick(20); EXPECT_EQ(2500, actualPower); +} +TEST_F(TrampTest, InvalidIndexDoesNotTransmitPower) { + custom(); start(); device->vTable->setPowerByIndex(device, 0); + device->vTable->setPowerByIndex(device, 5); tick(20); EXPECT_EQ(0, commands('P')); +} +TEST_F(TrampTest, HoleInCustomTableFallsBackToAutomatic) { + custom(); vtxSettingsConfig_System.trampPowerLevels[1] = 0; start(); EXPECT_EQ(5, device->capability.powerCount); +} +TEST_F(TrampTest, DecreasingCustomTableFallsBackToAutomatic) { + custom(); vtxSettingsConfig_System.trampPowerLevels[2] = 200; start(); EXPECT_EQ(5, device->capability.powerCount); +} +TEST_F(TrampTest, DuplicateCustomTableFallsBackToAutomatic) { + custom(); vtxSettingsConfig_System.trampPowerLevels[2] = 400; start(); EXPECT_EQ(5, device->capability.powerCount); +} +TEST_F(TrampTest, OutOfRangeCustomTableFallsBackToAutomatic) { + custom(); vtxSettingsConfig_System.trampPowerLevels[3] = 10001; start(); EXPECT_EQ(5, device->capability.powerCount); +} +TEST_F(TrampTest, FiveDigitLabelFits) { + custom(); vtxSettingsConfig_System.trampPowerLevels[4] = 10000; start(); + EXPECT_EQ(5, device->capability.powerCount); EXPECT_STREQ("10000", device->capability.powerNames[5]); +} +TEST_F(TrampTest, PitModeUsesCorrectCommandAndPolarity) { + start(); device->vTable->setPitMode(device, 1); tick(20); + EXPECT_TRUE(actualPit); EXPECT_GT(commands('I', 0), 0); + device->vTable->setPitMode(device, 0); tick(20); + EXPECT_FALSE(actualPit); EXPECT_GT(commands('I', 1), 0); + EXPECT_EQ(0, commands('s')); +} +TEST_F(TrampTest, LostPitCommandIsRetriedAfterStatusMismatch) { + start(); ignoreNextPit = true; device->vTable->setPitMode(device, 1); tick(40); + EXPECT_TRUE(actualPit); EXPECT_GE(commands('I', 0), 2); +} +TEST_F(TrampTest, LastPitRequestWinsBeforeTransmission) { + start(); device->vTable->setPitMode(device, 1); device->vTable->setPitMode(device, 0); tick(20); + EXPECT_FALSE(actualPit); EXPECT_EQ(0, commands('I', 0)); +} +TEST_F(TrampTest, HardwarePitModeIsPreservedWithoutExplicitRequest) { + actualPit = true; start(); tick(20); EXPECT_TRUE(actualPit); EXPECT_EQ(0, commands('I')); +} +TEST_F(TrampTest, CorruptStatusCannotChangeReportedPitMode) { + start(); uint8_t pit = 1; + actualPit = true; corruptNextStatus = true; + for (int i = 0; i < 30 && corruptNextStatus; ++i) tick(); + ASSERT_FALSE(corruptNextStatus); + tick(); // consume and reject the corrupt response + EXPECT_TRUE(device->vTable->getPitMode(device, &pit)); EXPECT_EQ(0, pit); + tick(20); // subsequent valid polls recover normally + EXPECT_TRUE(device->vTable->getPitMode(device, &pit)); EXPECT_EQ(1, pit); +} From d79d4d46794b910282a46f69241270cecc8f20e5 Mon Sep 17 00:00:00 2001 From: Raphael Hunziker Date: Sun, 13 Sep 2026 16:56:41 +0200 Subject: [PATCH 2/7] vtx: fix the pit mode switch that never appeared, and stop it starving the VTX Three defects in this pull request, found while going back over it. **The documented mode ID was wrong.** docs/VTx.md told the reader to assign permanent mode ID 69 and gave "aux 2 69 4 1800 2100" as the example. 69 is BOXAUTOSPEED; VTX PIT MODE is 72 (fc_msp_box.c:120). Following the instructions armed an autonomous speed mode on the switch instead of pit mode. Corrected in both places. **The switch never appeared.** initActiveBoxIds() gated BOXVTXPITMODE on feature(FEATURE_VTX). That flag has exactly two references in the whole tree: its own enum entry and that check. No target lists it in DEFAULT_FEATURES and COMMON_DEFAULT_FEATURES is (FEATURE_TX_PROF_SEL) alone, so on a stock configuration the mode was offered on no board at all. The gate is dropped; BOXVTXPITMODE is now added under USE_VTX_CONTROL the same way the camera boxes below it are, since VTX control is enabled by assigning a serial port function rather than by that flag. **Pit mode could starve channel and power indefinitely.** The IDLE dispatcher is a strict priority chain and pit mode was first in it, while every status response unconditionally restored the request whenever the reported state did not match. A Tramp-compatible device that will not enter pit mode - common, many only allow pit at power-up - therefore kept the highest-priority bit set forever, and no frequency or power command was ever sent again. Only a link timeout broke the loop, so a healthy link made it permanent. Frequency and power are now serviced before pit mode, and the requeue is bounded by VTX_PITMODE_MAX_RETRIES. The budget is refilled in impl_SetPitMode() only when the requested state actually changes, because io/vtx.c re-issues the same request roughly twice a second and would otherwise defeat the cap. --- docs/VTx.md | 4 ++-- src/main/fc/fc_msp_box.c | 8 +++++--- src/main/io/vtx_tramp.c | 41 ++++++++++++++++++++++++++++++---------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/docs/VTx.md b/docs/VTx.md index 29110943f25..610ca0d7016 100644 --- a/docs/VTx.md +++ b/docs/VTx.md @@ -66,11 +66,11 @@ to automatic selection. ### Tramp pit mode on an AUX switch -Assign **VTX PIT MODE** in Modes (permanent mode ID 69). For example, with the +Assign **VTX PIT MODE** in Modes (permanent mode ID 72). For example, with the first two mode-condition slots already in use, this assigns AUX5 / channel 9: ``` -aux 2 69 4 1800 2100 +aux 2 72 4 1800 2100 save ``` diff --git a/src/main/fc/fc_msp_box.c b/src/main/fc/fc_msp_box.c index 3b0070e779e..d44e04f26d2 100644 --- a/src/main/fc/fc_msp_box.c +++ b/src/main/fc/fc_msp_box.c @@ -339,9 +339,11 @@ void initActiveBoxIds(void) ADD_ACTIVE_BOX(BOXFAILSAFE); #ifdef USE_VTX_CONTROL - if (feature(FEATURE_VTX)) { - ADD_ACTIVE_BOX(BOXVTXPITMODE); - } + // No feature gate: FEATURE_VTX is vestigial in INAV. Nothing else in the + // tree reads it and no target enables it by default, so gating on it hid + // the switch on every board. VTX control is enabled by assigning a serial + // port function, the same way the camera boxes below are always offered. + ADD_ACTIVE_BOX(BOXVTXPITMODE); #endif #if defined(USE_RCDEVICE) || defined(USE_MSP_DISPLAYPORT) diff --git a/src/main/io/vtx_tramp.c b/src/main/io/vtx_tramp.c index 454742b9d30..a18c504bb81 100644 --- a/src/main/io/vtx_tramp.c +++ b/src/main/io/vtx_tramp.c @@ -53,6 +53,11 @@ #define VTX_UPDATE_REQ_POWER 0x02 #define VTX_UPDATE_REQ_PITMODE 0x04 +// A Tramp-compatible device that will not enter pit mode must not be asked +// forever: an unbounded requeue keeps the highest-priority request set and +// starves every pending channel and power change. +#define VTX_PITMODE_MAX_RETRIES 3 + typedef enum { VTX_STATE_RESET = 0, VTX_STATE_OFFILE = 1, // Not detected @@ -95,6 +100,7 @@ typedef struct { unsigned power; bool pitMode; bool pitModeRequested; // do not override hardware-button state before a request + uint8_t pitModeRetries; } request; // Actual VTX state: updated from actual VTX @@ -319,12 +325,7 @@ static void impl_Process(vtxDevice_t *vtxDevice, timeUs_t currentTimeUs) case VTX_STATE_IDLE: if (vtxState.updateReqMask != VTX_UPDATE_REQ_NONE) { // Updates pending. Send an appropriate command - if (vtxState.updateReqMask & VTX_UPDATE_REQ_PITMODE) { - vtxState.updateReqMask &= ~VTX_UPDATE_REQ_PITMODE; - vtxProtoSetPitMode(vtxState.request.pitMode); - vtxProtoSetState(VTX_STATE_QUERY_DELAY); - } - else if (vtxState.updateReqMask & VTX_UPDATE_REQ_FREQUENCY) { + if (vtxState.updateReqMask & VTX_UPDATE_REQ_FREQUENCY) { vtxState.updateReqMask &= ~VTX_UPDATE_REQ_FREQUENCY; vtxProtoSetFrequency(vtxState.request.freq); vtxProtoSetState(VTX_STATE_QUERY_DELAY); @@ -334,6 +335,13 @@ static void impl_Process(vtxDevice_t *vtxDevice, timeUs_t currentTimeUs) vtxProtoSetPower(vtxState.request.power); vtxProtoSetState(VTX_STATE_QUERY_DELAY); } + // Pit mode last: channel and power must go out even while a + // device keeps reporting a pit state we did not ask for. + else if (vtxState.updateReqMask & VTX_UPDATE_REQ_PITMODE) { + vtxState.updateReqMask &= ~VTX_UPDATE_REQ_PITMODE; + vtxProtoSetPitMode(vtxState.request.pitMode); + vtxProtoSetState(VTX_STATE_QUERY_DELAY); + } } else if ((millis() - vtxState.lastStatusQueryMs) > VTX_STATUS_INTERVAL_MS) { // Poll VTX for status updates @@ -371,7 +379,13 @@ static void impl_Process(vtxDevice_t *vtxDevice, timeUs_t currentTimeUs) } if (vtxState.request.pitModeRequested && vtxState.state.pitMode != vtxState.request.pitMode) { - vtxState.updateReqMask |= VTX_UPDATE_REQ_PITMODE; + if (vtxState.request.pitModeRetries < VTX_PITMODE_MAX_RETRIES) { + vtxState.request.pitModeRetries++; + vtxState.updateReqMask |= VTX_UPDATE_REQ_PITMODE; + } + } + else { + vtxState.request.pitModeRetries = 0; } // We got the status response - proceed to IDLE @@ -453,9 +467,16 @@ static void impl_SetPitMode(vtxDevice_t *vtxDevice, uint8_t onoff) { UNUSED(vtxDevice); - vtxState.request.pitMode = onoff != 0; - vtxState.request.pitModeRequested = true; - vtxState.updateReqMask |= VTX_UPDATE_REQ_PITMODE; + const bool newPitMode = onoff != 0; + + // io/vtx.c re-issues the same request about twice a second, so the retry + // budget may only be refilled when the pilot actually flips the switch. + if (!vtxState.request.pitModeRequested || vtxState.request.pitMode != newPitMode) { + vtxState.request.pitMode = newPitMode; + vtxState.request.pitModeRequested = true; + vtxState.request.pitModeRetries = 0; + vtxState.updateReqMask |= VTX_UPDATE_REQ_PITMODE; + } } static bool impl_GetBandAndChannel(const vtxDevice_t *vtxDevice, uint8_t *pBand, uint8_t *pChannel) From 2671dd7d3046fcfcb4405d56f5e96bc6ed08b5f0 Mon Sep 17 00:00:00 2001 From: Raffi1202 Date: Tue, 22 Sep 2026 23:26:45 +0200 Subject: [PATCH 3/7] Fix Tramp pit-mode lifecycle and preserve VTX settings --- docs/VTx.md | 24 ++++++++--- src/main/blackbox/blackbox.c | 5 +++ src/main/config/parameter_group.c | 7 ++++ src/main/config/vtx_settings_migration.h | 41 +++++++++++++++++++ src/main/drivers/vtx_common.h | 1 + src/main/fc/fc_init.c | 7 ++-- src/main/fc/fc_msp_box.c | 13 +++--- src/main/io/vtx.c | 13 ++++-- src/main/io/vtx_tramp.c | 31 +++++++++++--- src/test/unit/CMakeLists.txt | 3 ++ src/test/unit/vtx_pit_mode_unittest.cc | 31 ++++++++++++-- .../unit/vtx_settings_migration_unittest.cc | 29 +++++++++++++ src/test/unit/vtx_tramp_unittest.cc | 40 ++++++++++++++++-- 13 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 src/main/config/vtx_settings_migration.h create mode 100644 src/test/unit/vtx_settings_migration_unittest.cc diff --git a/docs/VTx.md b/docs/VTx.md index bf566b41cbb..7b454de09f8 100644 --- a/docs/VTx.md +++ b/docs/VTx.md @@ -54,15 +54,18 @@ save ``` This changes the power values and their labels, not the VTX's reported maximum. -The driver continues to clamp requests to that maximum. Only if the hardware's +The advertised custom table caps entries at that maximum and removes duplicate +capped levels, so each label matches the transmitted power value. Only if the hardware's maximum is independently confirmed and the device reports it incorrectly, use `vtx_max_power_override` to provide the correct maximum. It does not unlock the VTX or verify actual RF power. Power selection remains one-based: this example -maps levels 1–4 to 25, 400, 1000 and 2500 mW. +maps levels 1–4 to 25, 400, 1000 and 2500 mW when the effective maximum is +at least 2500 mW. A reported 400 mW maximum instead exposes only 25 and 400 mW. -The VTX settings parameter-group version changes from 2 to 3. Save `diff all` -before upgrading and restore the VTX settings afterwards; the new table defaults -to automatic selection. +The VTX settings parameter-group version changes from 2 to 3. Version-2 settings +are migrated with band, channel, power, low-power behavior, pit frequency, maximum +power override and frequency group preserved. The new table starts in automatic +selection mode. ### Tramp pit mode on an AUX switch @@ -78,6 +81,15 @@ The assigned range requests pit mode while disarmed. Arming exits pit mode and prevents entering it in flight. The switch is ignored without a valid receiver signal. Without a mode assignment, existing hardware-button/MSP control is left alone. Pit mode changes use the Tramp `I` command (0 = enter, 1 = exit), and the -driver retries if subsequent status reports do not match the request. Verify the +driver retries if subsequent status reports do not match the request. Retries are +bounded so an unsupported command cannot block channel or power changes, and a +reconnection renews the retry budget. Queued enter requests are cancelled on arming. +Blackbox records the AUX switch in `flightModeFlags3` (bit 0). Verify the VTX's own pit indicator before relying on it; driver support is not a guarantee that every Tramp-compatible device implements the command. + +The AUX pit-mode assignment is advertised only by drivers with functional pit-mode +read/write support (currently Tramp). Unsupported drivers do not receive AUX pit +commands. If a saved Tramp power index exceeds a shortened custom power table, +the runtime request uses its highest level, still capped by the device maximum; +the saved index is left unchanged. diff --git a/src/main/blackbox/blackbox.c b/src/main/blackbox/blackbox.c index 5d8ea57203b..e520292fe6a 100644 --- a/src/main/blackbox/blackbox.c +++ b/src/main/blackbox/blackbox.c @@ -496,6 +496,7 @@ static const blackboxSimpleFieldDefinition_t blackboxSlowFields[] = { {"activeWpNumber", -1, UNSIGNED, PREDICT(0), ENCODING(UNSIGNED_VB)}, {"flightModeFlags", -1, UNSIGNED, PREDICT(0), ENCODING(UNSIGNED_VB)}, {"flightModeFlags2", -1, UNSIGNED, PREDICT(0), ENCODING(UNSIGNED_VB)}, + {"flightModeFlags3", -1, UNSIGNED, PREDICT(0), ENCODING(UNSIGNED_VB)}, {"activeFlightModeFlags", -1, UNSIGNED, PREDICT(0), ENCODING(UNSIGNED_VB)}, {"stateFlags", -1, UNSIGNED, PREDICT(0), ENCODING(UNSIGNED_VB)}, @@ -625,6 +626,7 @@ typedef struct blackboxGpsState_s { typedef struct blackboxSlowState_s { uint32_t rcModeFlags; uint32_t rcModeFlags2; + uint32_t rcModeFlags3; uint32_t activeFlightModeFlags; uint32_t stateFlags; uint8_t failsafePhase; @@ -1430,6 +1432,7 @@ static void writeSlowFrame(void) blackboxWriteUnsignedVB(slowHistory.activeWpNumber); blackboxWriteUnsignedVB(slowHistory.rcModeFlags); blackboxWriteUnsignedVB(slowHistory.rcModeFlags2); + blackboxWriteUnsignedVB(slowHistory.rcModeFlags3); blackboxWriteUnsignedVB(slowHistory.activeFlightModeFlags); blackboxWriteUnsignedVB(slowHistory.stateFlags); @@ -1512,6 +1515,8 @@ static void loadSlowState(blackboxSlowState_t *slow) slow->rcModeFlags2 = rcModeActivationMask.bits[1]; // remaining bits of boxId_e #endif + slow->rcModeFlags3 = rcModeActivationMask.bits[2]; // box IDs 64 and above + // Also log Nav auto enabled flight modes rather than just those selected by boxmode if (navigationGetHeadingControlState() == NAV_HEADING_CONTROL_AUTO) { slow->rcModeFlags |= (1 << BOXHEADINGHOLD); diff --git a/src/main/config/parameter_group.c b/src/main/config/parameter_group.c index 997fddb8eff..69cb0486a32 100644 --- a/src/main/config/parameter_group.c +++ b/src/main/config/parameter_group.c @@ -22,6 +22,9 @@ #include "platform.h" #include "parameter_group.h" +#ifdef USE_VTX_CONTROL +#include "config/vtx_settings_migration.h" +#endif #include "common/maths.h" const pgRegistry_t* pgFind(pgn_t pgn) @@ -90,6 +93,10 @@ void pgLoad(const pgRegistry_t* reg, int profileIndex, const void *from, int siz if (version == pgVersion(reg)) { const int take = MIN(size, pgSize(reg)); memcpy(pgOffset(reg, profileIndex), from, take); +#ifdef USE_VTX_CONTROL + } else { + pgMigrateVtxSettings(reg, pgOffset(reg, profileIndex), from, size, version); +#endif } } diff --git a/src/main/config/vtx_settings_migration.h b/src/main/config/vtx_settings_migration.h new file mode 100644 index 00000000000..3b957f9cba8 --- /dev/null +++ b/src/main/config/vtx_settings_migration.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include "config/parameter_group.h" +#include "config/parameter_group_ids.h" +#include "io/vtx.h" + +// Version 2 placed frequencyGroup immediately after maxPowerOverride. +// Version 3 inserts the optional Tramp table before frequencyGroup. +typedef struct vtxSettingsConfigV2_s { + uint8_t band; + uint8_t channel; + uint8_t power; + uint16_t pitModeChan; + uint8_t lowPowerDisarm; + uint16_t maxPowerOverride; + uint8_t frequencyGroup; +} vtxSettingsConfigV2_t; + +static inline bool pgMigrateVtxSettings(const pgRegistry_t *reg, void *to, + const void *from, int size, int version) +{ + if (pgN(reg) != PG_VTX_SETTINGS_CONFIG || pgVersion(reg) != 3 || version != 2 || + pgSize(reg) != sizeof(vtxSettingsConfig_t) || size != sizeof(vtxSettingsConfigV2_t)) { + return false; + } + // Copy from potentially unaligned EEPROM data through a local old record. + vtxSettingsConfigV2_t old; + memcpy(&old, from, sizeof(old)); + vtxSettingsConfig_t *dest = (vtxSettingsConfig_t *)to; + dest->band = old.band; + dest->channel = old.channel; + dest->power = old.power; + dest->pitModeChan = old.pitModeChan; + dest->lowPowerDisarm = old.lowPowerDisarm; + dest->maxPowerOverride = old.maxPowerOverride; + dest->frequencyGroup = old.frequencyGroup; + memset(dest->trampPowerLevels, 0, sizeof(dest->trampPowerLevels)); + return true; +} diff --git a/src/main/drivers/vtx_common.h b/src/main/drivers/vtx_common.h index 83e608dc49b..c6057a2c920 100644 --- a/src/main/drivers/vtx_common.h +++ b/src/main/drivers/vtx_common.h @@ -75,6 +75,7 @@ typedef struct vtxDeviceCapability_s { uint8_t bandCount; uint8_t channelCount; uint8_t powerCount; + bool supportsPitMode; // Functional read and write support for AUX control. char **bandNames; char **channelNames; char **powerNames; diff --git a/src/main/fc/fc_init.c b/src/main/fc/fc_init.c index 9ba5a8e442c..d79394aa3a7 100644 --- a/src/main/fc/fc_init.c +++ b/src/main/fc/fc_init.c @@ -598,10 +598,6 @@ void init(void) imuInit(); - // Sensors have now been detected, mspFcInit() can now be called - // to set the boxes up - mspFcInit(); - cliInit(serialConfig()); failsafeInit(); @@ -754,6 +750,9 @@ void init(void) #endif // USE_VTX_CONTROL + // Advertised modes depend on both detected sensors and initialized VTX capabilities. + mspFcInit(); + // Now that everything has powered up the voltage and cell count be determined. if (feature(FEATURE_VBAT | FEATURE_CURRENT_METER)) batteryInit(); diff --git a/src/main/fc/fc_msp_box.c b/src/main/fc/fc_msp_box.c index f7f83239c99..e83a2aa0cb2 100644 --- a/src/main/fc/fc_msp_box.c +++ b/src/main/fc/fc_msp_box.c @@ -35,6 +35,10 @@ #include "io/osd.h" +#ifdef USE_VTX_CONTROL +#include "drivers/vtx_common.h" +#endif + #include "drivers/pwm_mapping.h" #include "drivers/pwm_output.h" @@ -342,11 +346,10 @@ void initActiveBoxIds(void) ADD_ACTIVE_BOX(BOXFAILSAFE); #ifdef USE_VTX_CONTROL - // No feature gate: FEATURE_VTX is vestigial in INAV. Nothing else in the - // tree reads it and no target enables it by default, so gating on it hid - // the switch on every board. VTX control is enabled by assigning a serial - // port function, the same way the camera boxes below are always offered. - ADD_ACTIVE_BOX(BOXVTXPITMODE); + const vtxDevice_t *vtxDevice = vtxCommonDevice(); + if (vtxDevice && vtxDevice->capability.supportsPitMode) { + ADD_ACTIVE_BOX(BOXVTXPITMODE); + } #endif #if defined(USE_RCDEVICE) || defined(USE_MSP_DISPLAYPORT) diff --git a/src/main/io/vtx.c b/src/main/io/vtx.c index c9ecb9a9aeb..798cae0d7f8 100644 --- a/src/main/io/vtx.c +++ b/src/main/io/vtx.c @@ -119,8 +119,14 @@ static bool vtxProcessPower(vtxDevice_t *vtxDevice, const vtxSettingsConfig_t * return false; } - if (vtxPower != runtimeSettings->power) { - vtxCommonSetPowerByIndex(vtxDevice, runtimeSettings->power); + // Clamp before the common setter, which rejects indices beyond the device's + // table. A saved level can outlive a shorter custom Tramp power table. + uint8_t requestedPower = runtimeSettings->power; + if (vtxCommonGetDeviceType(vtxDevice) == VTXDEV_TRAMP && vtxDevice->capability.powerCount) { + requestedPower = MIN(requestedPower, vtxDevice->capability.powerCount); + } + if (vtxPower != requestedPower) { + vtxCommonSetPowerByIndex(vtxDevice, requestedPower); return true; } @@ -132,7 +138,8 @@ static bool vtxProcessPitMode(vtxDevice_t *vtxDevice, const vtxSettingsConfig_t UNUSED(runtimeSettings); // Leave button/MSP control alone unless the pilot assigned this mode. - if (!isModeActivationConditionPresent(BOXVTXPITMODE) || !rxIsReceivingSignal()) { + if (!vtxDevice->capability.supportsPitMode || + !isModeActivationConditionPresent(BOXVTXPITMODE) || !rxIsReceivingSignal()) { return false; } diff --git a/src/main/io/vtx_tramp.c b/src/main/io/vtx_tramp.c index a18c504bb81..38b50a60b87 100644 --- a/src/main/io/vtx_tramp.c +++ b/src/main/io/vtx_tramp.c @@ -42,6 +42,7 @@ #include "io/vtx_tramp.h" #include "io/vtx_control.h" #include "io/vtx.h" +#include "fc/runtime_config.h" #include "io/vtx_string.h" #define VTX_PKT_SIZE 16 @@ -289,9 +290,19 @@ static void impl_Process(vtxDevice_t *vtxDevice, timeUs_t currentTimeUs) return; } + // Arming can occur after the AUX scheduler queued an enter request. Cancel + // it before either dispatch or mismatch retries can transmit it in flight. + if (ARMING_FLAG(ARMED) && vtxState.request.pitMode) { + vtxState.request.pitMode = false; + vtxState.request.pitModeRequested = true; + vtxState.request.pitModeRetries = 0; + vtxState.updateReqMask |= VTX_UPDATE_REQ_PITMODE; + } + switch((int)vtxState.protoState) { case VTX_STATE_RESET: vtxState.protoTimeoutCount = 0; + vtxState.request.pitModeRetries = 0; vtxState.updateReqMask = VTX_UPDATE_REQ_NONE; vtxProtoSetState(VTX_STATE_OFFILE); break; @@ -467,7 +478,7 @@ static void impl_SetPitMode(vtxDevice_t *vtxDevice, uint8_t onoff) { UNUSED(vtxDevice); - const bool newPitMode = onoff != 0; + const bool newPitMode = onoff != 0 && !ARMING_FLAG(ARMED); // io/vtx.c re-issues the same request about twice a second, so the retry // budget may only be refilled when the pilot actually flips the switch. @@ -583,6 +594,7 @@ static vtxDevice_t impl_vtxDevice = { .capability.bandCount = VTX_TRAMP_5G8_BAND_COUNT, .capability.channelCount = VTX_TRAMP_5G8_CHANNEL_COUNT, .capability.powerCount = VTX_TRAMP_5G8_MAX_POWER_COUNT, + .capability.supportsPitMode = true, .capability.bandNames = (char **)vtx58BandNames, .capability.channelNames = (char **)vtx58ChannelNames, .capability.powerNames = NULL, @@ -606,6 +618,7 @@ const char * const trampPowerNames_1G3_800[VTX_TRAMP_1G3_MAX_POWER_COUNT + 1] = const uint16_t trampPowerTable_1G3_2000[VTX_TRAMP_1G3_MAX_POWER_COUNT] = { 25, 200, 2000 }; const char * const trampPowerNames_1G3_2000[VTX_TRAMP_1G3_MAX_POWER_COUNT + 1] = { "---", "25 ", "200", "2000" }; +static uint16_t customPowerLevels[VTX_TRAMP_5G8_MAX_POWER_COUNT]; static char customPowerNames[VTX_TRAMP_5G8_MAX_POWER_COUNT][6]; static char *customPowerNamePointers[VTX_TRAMP_5G8_MAX_POWER_COUNT + 1]; @@ -629,12 +642,20 @@ static bool vtxProtoUseCustomPowerTable(void) return false; } + const unsigned configuredCount = count; + count = 0; customPowerNamePointers[0] = "---"; - for (unsigned i = 0; i < count; i++) { - snprintf(customPowerNames[i], sizeof(customPowerNames[i]), "%u", (unsigned)levels[i]); - customPowerNamePointers[i + 1] = customPowerNames[i]; + for (unsigned i = 0; i < configuredCount; i++) { + const uint16_t effectivePower = MIN(levels[i], vtxState.capabilities.powerMax); + if (count && effectivePower == customPowerLevels[count - 1]) { + continue; + } + customPowerLevels[count] = effectivePower; + snprintf(customPowerNames[count], sizeof(customPowerNames[count]), "%u", (unsigned)effectivePower); + customPowerNamePointers[count + 1] = customPowerNames[count]; + count++; } - vtxState.metadata.powerTablePtr = levels; + vtxState.metadata.powerTablePtr = customPowerLevels; vtxState.metadata.powerTableCount = count; impl_vtxDevice.capability.powerCount = count; impl_vtxDevice.capability.powerNames = customPowerNamePointers; diff --git a/src/test/unit/CMakeLists.txt b/src/test/unit/CMakeLists.txt index 02784feab06..61fb308ab27 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -189,6 +189,9 @@ set_property(SOURCE vtx_pit_mode_unittest.cc PROPERTY depends "io/vtx.c") set_property(SOURCE vtx_tramp_unittest.cc PROPERTY definitions USE_VTX_CONTROL USE_VTX_TRAMP) set_property(SOURCE vtx_tramp_unittest.cc PROPERTY depends "io/vtx_tramp.c" "io/vtx_string.c" "common/crc.c" "common/streambuf.c") +set_property(SOURCE vtx_settings_migration_unittest.cc PROPERTY depends "config/parameter_group.c") +set_property(SOURCE vtx_settings_migration_unittest.cc PROPERTY definitions USE_VTX_CONTROL) + function(unit_test src) get_filename_component(basename ${src} NAME) string(REPLACE ".cc" "" name ${basename} ) diff --git a/src/test/unit/vtx_pit_mode_unittest.cc b/src/test/unit/vtx_pit_mode_unittest.cc index 5d7aa2b55f5..7ea21fcac77 100644 --- a/src/test/unit/vtx_pit_mode_unittest.cc +++ b/src/test/unit/vtx_pit_mode_unittest.cc @@ -29,7 +29,9 @@ uint32_t armingFlags; uint8_t cliMode; } static bool assigned, receiving, modeActive, pit; -static int requests; +static int requests, powerRequests; +static uint8_t power; +static vtxDevType_e deviceType; static vtxDevice_t device{}; extern "C" { bool isModeActivationConditionPresent(boxId_e id) { return id == BOXVTXPITMODE && assigned; } @@ -40,8 +42,12 @@ void vtxControlInputPoll(void) {} vtxDevice_t *vtxCommonDevice(void) { return &device; } bool vtxCommonGetPitMode(vtxDevice_t *, uint8_t *value) { *value = pit; return true; } void vtxCommonSetPitMode(vtxDevice_t *, uint8_t value) { pit = value; ++requests; } -bool vtxCommonGetPowerIndex(vtxDevice_t *, uint8_t *) { return false; } -void vtxCommonSetPowerByIndex(vtxDevice_t *, uint8_t) {} +vtxDevType_e vtxCommonGetDeviceType(vtxDevice_t *) { return deviceType; } +bool vtxCommonGetPowerIndex(vtxDevice_t *, uint8_t *value) { *value = power; return true; } +void vtxCommonSetPowerByIndex(vtxDevice_t *d, uint8_t value) { + if (value > d->capability.powerCount) return; + power = value; ++powerRequests; +} bool vtxCommonGetBandAndChannel(vtxDevice_t *, uint8_t *, uint8_t *) { return false; } void vtxCommonSetBandAndChannel(vtxDevice_t *, uint8_t, uint8_t) {} void vtxCommonProcess(vtxDevice_t *, timeUs_t) {} @@ -51,6 +57,10 @@ class PitModeTest : public ::testing::Test { void SetUp() override { assigned = true; receiving = true; modeActive = false; pit = false; requests = 0; armingFlags = 0; cliMode = 0; + device.capability.supportsPitMode = true; + device.capability.powerCount = 3; + deviceType = VTXDEV_TRAMP; power = 1; powerRequests = 0; + vtxSettingsConfigMutable()->power = 1; } void update() { for (int i=0; i<3; ++i) vtxUpdate(0); } }; @@ -76,3 +86,18 @@ TEST_F(PitModeTest, NoCommandWhenReportedStateMatchesSwitch) { TEST_F(PitModeTest, CliDoesNotSendPitCommands) { cliMode=1; modeActive=true; update(); EXPECT_FALSE(pit); EXPECT_EQ(0,requests); } + +TEST_F(PitModeTest, UnsupportedDeviceDoesNotReceivePitRequests) { + device.capability.supportsPitMode=false; modeActive=true; update(); + EXPECT_FALSE(pit); EXPECT_EQ(0,requests); +} +TEST_F(PitModeTest, SavedPowerAboveShorterTrampTableIsClampedOnce) { + vtxSettingsConfigMutable()->power=5; update(); + EXPECT_EQ(3,power); EXPECT_EQ(1,powerRequests); + update(); EXPECT_EQ(1,powerRequests); + EXPECT_EQ(5,vtxSettingsConfig()->power); +} +TEST_F(PitModeTest, OtherDevicePowerSemanticsAreUnchanged) { + deviceType=VTXDEV_SMARTAUDIO; vtxSettingsConfigMutable()->power=5; update(); + EXPECT_EQ(1,power); EXPECT_EQ(0,powerRequests); +} diff --git a/src/test/unit/vtx_settings_migration_unittest.cc b/src/test/unit/vtx_settings_migration_unittest.cc new file mode 100644 index 00000000000..cdcfd6e5ce2 --- /dev/null +++ b/src/test/unit/vtx_settings_migration_unittest.cc @@ -0,0 +1,29 @@ +#include +#include "gtest/gtest.h" +extern "C" { +#include "platform.h" +#include "config/vtx_settings_migration.h" +PG_REGISTER_WITH_RESET_TEMPLATE(uint32_t, migrationTestSection, 4090, 0); +PG_RESET_TEMPLATE(uint32_t, migrationTestSection, 0); +} +TEST(VtxSettingsMigration, LoaderPreservesEveryOldFieldAndStartsWithAutomaticTable) { + vtxSettingsConfigV2_t old{}; + old.band=5; old.channel=8; old.power=4; old.pitModeChan=5658; + old.lowPowerDisarm=2; old.maxPowerOverride=2500; old.frequencyGroup=2; + vtxSettingsConfig_t dest; + memset(&dest,0xff,sizeof(dest)); + pgRegistry_t reg{}; reg.pgn=PG_VTX_SETTINGS_CONFIG | (3<<12); + reg.size=sizeof(dest); reg.address=reinterpret_cast(&dest); + pgLoad(®,0,&old,sizeof(old),2); + EXPECT_EQ(old.band,dest.band); EXPECT_EQ(old.channel,dest.channel); EXPECT_EQ(old.power,dest.power); + EXPECT_EQ(old.pitModeChan,dest.pitModeChan); EXPECT_EQ(old.lowPowerDisarm,dest.lowPowerDisarm); + EXPECT_EQ(old.maxPowerOverride,dest.maxPowerOverride); EXPECT_EQ(old.frequencyGroup,dest.frequencyGroup); + for(auto level:dest.trampPowerLevels) EXPECT_EQ(0,level); + const auto current=dest; + pgLoad(®,0,¤t,sizeof(current),3); + EXPECT_EQ(0,memcmp(¤t,&dest,sizeof(dest))); + EXPECT_FALSE(pgMigrateVtxSettings(®,&dest,&old,sizeof(old)-1,2)); + EXPECT_FALSE(pgMigrateVtxSettings(®,&dest,&old,sizeof(old),1)); + reg.pgn=PG_VTX_SETTINGS_CONFIG | (4<<12); + EXPECT_FALSE(pgMigrateVtxSettings(®,&dest,&old,sizeof(old),3)); +} diff --git a/src/test/unit/vtx_tramp_unittest.cc b/src/test/unit/vtx_tramp_unittest.cc index 32f28ea556e..68567862e8d 100644 --- a/src/test/unit/vtx_tramp_unittest.cc +++ b/src/test/unit/vtx_tramp_unittest.cc @@ -33,6 +33,8 @@ extern "C" { #include "io/vtx.h" #include "io/vtx_control.h" #include "io/vtx_tramp.h" +#include "fc/runtime_config.h" +uint32_t armingFlags; vtxSettingsConfig_t vtxSettingsConfig_System; vtxConfig_t vtxConfig_System; } @@ -44,7 +46,8 @@ static std::deque received; static std::vector> sent; static timeMs_t now; static uint16_t reportedMax, actualPower, actualFrequency; -static bool actualPit, ignoreNextPit, corruptNextStatus; +static bool connected; +static bool actualPit, ignoreNextPit, ignoreAllPit, corruptNextStatus; static void respond(char command, uint16_t a, uint16_t b, uint16_t c) { @@ -80,12 +83,14 @@ void serialWriteBuf(serialPort_t *, const uint8_t *data, int size) EXPECT_EQ(0, data[15]); sent.push_back(frame); uint16_t value = data[2] | (data[3] << 8); + if (!connected) return; switch (data[1]) { case 'r': respond('r', 5000, 5999, reportedMax); break; case 'v': respond('v', actualFrequency, actualPower, 0); break; case 'F': actualFrequency = value; break; case 'P': actualPower = value; break; case 'I': + if (ignoreAllPit) break; if (ignoreNextPit) ignoreNextPit = false; else actualPit = value == 0; break; @@ -98,8 +103,9 @@ class TrampTest : public ::testing::Test { void SetUp() override { std::memset(&vtxSettingsConfig_System, 0, sizeof(vtxSettingsConfig_System)); std::memset(&vtxConfig_System, 0, sizeof(vtxConfig_System)); + armingFlags = 0; connected = true; now = 0; reportedMax = 2500; actualPower = 25; actualFrequency = 5732; - actualPit = false; ignoreNextPit = false; corruptNextStatus = false; + actualPit = false; ignoreNextPit = false; ignoreAllPit = false; corruptNextStatus = false; received.clear(); sent.clear(); device = nullptr; } void tick(int n = 1) { @@ -141,7 +147,9 @@ TEST_F(TrampTest, CustomTableUsesAllFourBlitzPowers) { } TEST_F(TrampTest, CustomPowerStillRespectsReportedLimit) { custom(); reportedMax = 400; start(); - device->vTable->setPowerByIndex(device, 4); tick(20); EXPECT_EQ(400, actualPower); + ASSERT_EQ(2,device->capability.powerCount); + EXPECT_STREQ("400",device->capability.powerNames[2]); + device->vTable->setPowerByIndex(device, 2); tick(20); EXPECT_EQ(400, actualPower); } TEST_F(TrampTest, ExplicitOverrideAllowsConfiguredMaximum) { custom(); reportedMax = 400; vtxSettingsConfig_System.maxPowerOverride = 2500; start(); @@ -164,7 +172,7 @@ TEST_F(TrampTest, OutOfRangeCustomTableFallsBackToAutomatic) { custom(); vtxSettingsConfig_System.trampPowerLevels[3] = 10001; start(); EXPECT_EQ(5, device->capability.powerCount); } TEST_F(TrampTest, FiveDigitLabelFits) { - custom(); vtxSettingsConfig_System.trampPowerLevels[4] = 10000; start(); + custom(); reportedMax = 10000; vtxSettingsConfig_System.trampPowerLevels[4] = 10000; start(); EXPECT_EQ(5, device->capability.powerCount); EXPECT_STREQ("10000", device->capability.powerNames[5]); } TEST_F(TrampTest, PitModeUsesCorrectCommandAndPolarity) { @@ -195,3 +203,27 @@ TEST_F(TrampTest, CorruptStatusCannotChangeReportedPitMode) { tick(20); // subsequent valid polls recover normally EXPECT_TRUE(device->vTable->getPitMode(device, &pit)); EXPECT_EQ(1, pit); } + +TEST_F(TrampTest, PermanentPitFailureDoesNotBlockPowerOrChannelAndRetriesStop) { + start(); ASSERT_TRUE(device->capability.supportsPitMode); ignoreAllPit=true; + device->vTable->setPitMode(device,1); + device->vTable->setPowerByIndex(device,2); + device->vTable->setBandAndChannel(device,1,1); + tick(100); + EXPECT_GT(commands('P'),0); EXPECT_GT(commands('F'),0); + const int retries=commands('I'); EXPECT_GT(retries,0); EXPECT_LE(retries,4); + for (int i=0;i<100;i++) { device->vTable->setPitMode(device,1); tick(); } + EXPECT_EQ(retries,commands('I')); +} + +TEST_F(TrampTest, ArmingCancelsQueuedEnterBeforeDispatch) { + start(); device->vTable->setPitMode(device,1); armingFlags=ARMED; tick(50); + EXPECT_FALSE(actualPit); EXPECT_EQ(0,commands('I',0)); + device->vTable->setPitMode(device,1); tick(50); EXPECT_EQ(0,commands('I',0)); +} +TEST_F(TrampTest, ReconnectRenewsExhaustedPitRetryBudget) { + start(); ignoreAllPit=true; device->vTable->setPitMode(device,1); tick(100); + const int before=commands('I'); ASSERT_GT(before,0); + connected=false; tick(100); connected=true; ignoreAllPit=false; tick(100); + EXPECT_TRUE(actualPit); EXPECT_GT(commands('I'),before); +} From 9ddf907173ab7fbf78ff63efb20cdac034e05823 Mon Sep 17 00:00:00 2001 From: Raffi1202 Date: Tue, 22 Sep 2026 23:37:06 +0200 Subject: [PATCH 4/7] Provide ELF section boundaries for PG migration host tests --- src/test/unit/CMakeLists.txt | 5 +++++ src/test/unit/parameter_group_test.ld | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/test/unit/parameter_group_test.ld diff --git a/src/test/unit/CMakeLists.txt b/src/test/unit/CMakeLists.txt index 61fb308ab27..60cae2f742f 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -240,3 +240,8 @@ foreach(source ${TEST_PROGRAMS}) endforeach() add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS ${test_targets}) + +# ELF does not synthesize the firmware PG section boundaries for host tests. +if(UNIX AND NOT APPLE) + target_link_options(vtx_settings_migration_unittest PRIVATE "-Wl,-T,${CMAKE_CURRENT_SOURCE_DIR}/parameter_group_test.ld") +endif() diff --git a/src/test/unit/parameter_group_test.ld b/src/test/unit/parameter_group_test.ld new file mode 100644 index 00000000000..201bd31849f --- /dev/null +++ b/src/test/unit/parameter_group_test.ld @@ -0,0 +1,18 @@ +/* Host tests linking parameter_group.c need the PG section boundaries that + * firmware linker scripts normally provide. Mach-O supplies these automatically. */ +SECTIONS +{ + .pg_registry : ALIGN(8) + { + __pg_registry_start = .; + KEEP(*(.pg_registry)) + __pg_registry_end = .; + } + .pg_resetdata : ALIGN(2) + { + __pg_resetdata_start = .; + KEEP(*(.pg_resetdata)) + __pg_resetdata_end = .; + } +} +INSERT AFTER .data; From 43e8b721d68ec615b541a4a6ca96b8806ba31df7 Mon Sep 17 00:00:00 2001 From: Raffi1202 Date: Tue, 22 Sep 2026 23:57:40 +0200 Subject: [PATCH 5/7] Wait for Tramp power capabilities before publishing custom levels --- src/main/io/vtx_tramp.c | 5 +++++ src/test/unit/vtx_tramp_unittest.cc | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/io/vtx_tramp.c b/src/main/io/vtx_tramp.c index 38b50a60b87..856fc514f59 100644 --- a/src/main/io/vtx_tramp.c +++ b/src/main/io/vtx_tramp.c @@ -624,6 +624,11 @@ static char *customPowerNamePointers[VTX_TRAMP_5G8_MAX_POWER_COUNT + 1]; static bool vtxProtoUseCustomPowerTable(void) { + // Keep the startup table until the device supplies a usable power limit. + if (!vtxState.capabilities.powerMax) { + return false; + } + const uint16_t *levels = vtxSettingsConfig()->trampPowerLevels; unsigned count = 0; bool ended = false; diff --git a/src/test/unit/vtx_tramp_unittest.cc b/src/test/unit/vtx_tramp_unittest.cc index 68567862e8d..bde406064fd 100644 --- a/src/test/unit/vtx_tramp_unittest.cc +++ b/src/test/unit/vtx_tramp_unittest.cc @@ -135,6 +135,18 @@ TEST_F(TrampTest, DefaultTableRemainsAutomatic) { start(); EXPECT_EQ(5, device->capability.powerCount); EXPECT_STREQ("800", device->capability.powerNames[5]); } +TEST_F(TrampTest, CustomTableWaitsForReportedPowerLimit) { + custom(); + ASSERT_TRUE(vtxTrampInit()); + EXPECT_FALSE(device->vTable->isReady(device)); + ASSERT_EQ(5, device->capability.powerCount); + EXPECT_STREQ("25 ", device->capability.powerNames[1]); + EXPECT_STREQ("600", device->capability.powerNames[5]); + tick(3); + ASSERT_TRUE(device->vTable->isReady(device)); + ASSERT_EQ(4, device->capability.powerCount); + EXPECT_STREQ("2500", device->capability.powerNames[4]); +} TEST_F(TrampTest, CustomTableUsesAllFourBlitzPowers) { custom(); start(); ASSERT_EQ(4, device->capability.powerCount); const char *labels[] = {"25", "400", "1000", "2500"}; From 5c51bee580978d8a17af9fbb8fba909fb8782d43 Mon Sep 17 00:00:00 2001 From: Raffi1202 Date: Wed, 23 Sep 2026 00:24:10 +0200 Subject: [PATCH 6/7] Refresh CMS power capabilities and fairly schedule Tramp updates --- docs/VTx.md | 5 +- src/main/cms/cms.c | 9 ++ src/main/cms/cms_menu_vtx.c | 28 ++++++- src/main/cms/cms_menu_vtx.h | 2 + src/main/cms/cms_types.h | 2 +- src/main/io/vtx_tramp.c | 30 ++++--- src/test/unit/CMakeLists.txt | 2 + src/test/unit/cms_vtx_unittest.cc | 123 ++++++++++++++++++++++++++++ src/test/unit/vtx_tramp_unittest.cc | 22 ++++- 9 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 src/test/unit/cms_vtx_unittest.cc diff --git a/docs/VTx.md b/docs/VTx.md index 7b454de09f8..57033568644 100644 --- a/docs/VTx.md +++ b/docs/VTx.md @@ -83,7 +83,10 @@ signal. Without a mode assignment, existing hardware-button/MSP control is left alone. Pit mode changes use the Tramp `I` command (0 = enter, 1 = exit), and the driver retries if subsequent status reports do not match the request. Retries are bounded so an unsupported command cannot block channel or power changes, and a -reconnection renews the retry budget. Queued enter requests are cancelled on arming. +reconnection renews the retry budget. Pending frequency, power and pit commands +are served in rotation so a rejected setting cannot starve another request. +Queued enter requests are cancelled on arming. The CMS power selection follows +the detected table and is clamped to its available levels before display or save. Blackbox records the AUX switch in `flightModeFlags3` (bit 0). Verify the VTX's own pit indicator before relying on it; driver support is not a guarantee that every Tramp-compatible device implements the command. diff --git a/src/main/cms/cms.c b/src/main/cms/cms.c index 728b7637112..a5b2d63e498 100644 --- a/src/main/cms/cms.c +++ b/src/main/cms/cms.c @@ -70,6 +70,10 @@ #include "io/osd.h" #include "io/rcdevice_cam.h" +#ifdef USE_VTX_CONTROL +#include "cms/cms_menu_vtx.h" +#endif + #include "rx/rx.h" // DisplayPort management @@ -1591,6 +1595,11 @@ void cmsUpdate(timeUs_t currentTimeUs) // Only scan keys and draw if we're not yielding if (cmsYieldUntil == 0) { +#ifdef USE_VTX_CONTROL + if (currentCtx.menu == &cmsx_menuVtxControl) { + cmsVtxUpdatePowerMetadata(); + } +#endif // XXX: Note that one call to cmsScanKeys() might generate multiple keypresses // when repeating, that's why cmsYieldDisplay() has to check for multiple calls. rcDelayMs = cmsScanKeys(currentTimeMs, lastCalledMs, rcDelayMs); diff --git a/src/main/cms/cms_menu_vtx.c b/src/main/cms/cms_menu_vtx.c index a56656ec8a1..494bcd72b96 100644 --- a/src/main/cms/cms_menu_vtx.c +++ b/src/main/cms/cms_menu_vtx.c @@ -27,6 +27,7 @@ #if defined(USE_CMS) && defined(USE_VTX_CONTROL) #include "common/printf.h" +#include "common/maths.h" #include "common/utils.h" #include "cms/cms.h" @@ -57,6 +58,22 @@ static OSD_TAB_t cms_Vtx_EntBand = { &vtxBand, VTX_SETTINGS_BAND_COUNT, vtx58Ban static OSD_TAB_t cms_Vtx_EntChan = { &vtxChan, VTX_SETTINGS_CHANNEL_COUNT, vtx58ChannelNames }; static OSD_TAB_t cms_Vtx_EntPower = { &vtxPower, VTX_SETTINGS_POWER_COUNT, vtx58DefaultPowerNames }; static const OSD_TAB_t cms_Vtx_EntPitMode = { &vtxPitMode, 2, vtxCmsPitModeNames }; +static const char * const vtxUnknownPowerNames[] = { "---" }; + +void cmsVtxUpdatePowerMetadata(void) +{ + vtxDeviceCapability_t capability; + if (vtxCommonGetDeviceCapability(vtxCommonDevice(), &capability) + && capability.powerCount && capability.powerNames) { + cms_Vtx_EntPower.max = capability.powerCount; + cms_Vtx_EntPower.names = (const char * const *)capability.powerNames; + } else { + cms_Vtx_EntPower.max = 0; + cms_Vtx_EntPower.names = vtxUnknownPowerNames; + } + // Clamp the menu selection, preserving EEPROM until the pilot saves it. + vtxPower = MIN(vtxPower, cms_Vtx_EntPower.max); +} static long cms_Vtx_configPitMode(displayPort_t *pDisp, const void *self) { @@ -100,7 +117,7 @@ static long cms_Vtx_configPower(displayPort_t *pDisp, const void *self) UNUSED(pDisp); UNUSED(self); - if (vtxPower == 0) { + if (vtxPower == 0 && cms_Vtx_EntPower.max > 0) { vtxPower = 1; } return 0; @@ -135,6 +152,7 @@ static void cms_Vtx_initSettings(void) vtxBand = vtxSettingsConfig()->band; vtxChan = vtxSettingsConfig()->channel; vtxPower = vtxSettingsConfig()->power; + cmsVtxUpdatePowerMetadata(); // If device is ready - read actual PIT mode if (vtxCommonDeviceIsReady(vtxDevice)) { @@ -159,6 +177,12 @@ static long cms_Vtx_Commence(displayPort_t *pDisp, const void *self) UNUSED(pDisp); UNUSED(self); + cmsVtxUpdatePowerMetadata(); + + if (!cms_Vtx_EntPower.max) { + return MENU_CHAIN_BACK; + } + vtxCommonSetBandAndChannel(vtxCommonDevice(), vtxBand, vtxChan); vtxCommonSetPowerByIndex(vtxCommonDevice(), vtxPower); vtxCommonSetPitMode(vtxCommonDevice(), vtxPitMode == 2 ? 1 : 0); @@ -243,7 +267,7 @@ static const OSD_Entry cms_menuVtxEntries[] = OSD_TAB_CALLBACK_ENTRY("PIT", cms_Vtx_configPitMode, &cms_Vtx_EntPitMode), OSD_TAB_CALLBACK_ENTRY("BAND", cms_Vtx_configBand, &cms_Vtx_EntBand), OSD_TAB_CALLBACK_ENTRY("CHAN", cms_Vtx_configChan, &cms_Vtx_EntChan), - OSD_TAB_CALLBACK_ENTRY("POWER", cms_Vtx_configPower, &cms_Vtx_EntPower), + { "POWER", {.func = cms_Vtx_configPower}, &cms_Vtx_EntPower, OME_TAB, DYNAMIC }, OSD_SUBMENU_ENTRY("SET", &cms_menuCommence), OSD_BACK_AND_END_ENTRY, diff --git a/src/main/cms/cms_menu_vtx.h b/src/main/cms/cms_menu_vtx.h index cadad8be458..d0403be5ee1 100644 --- a/src/main/cms/cms_menu_vtx.h +++ b/src/main/cms/cms_menu_vtx.h @@ -18,3 +18,5 @@ #pragma once extern const CMS_Menu cmsx_menuVtxControl; + +void cmsVtxUpdatePowerMetadata(void); diff --git a/src/main/cms/cms_types.h b/src/main/cms/cms_types.h index a07e55eaaa4..c1890c1da3c 100644 --- a/src/main/cms/cms_types.h +++ b/src/main/cms/cms_types.h @@ -123,7 +123,7 @@ typedef enum { } CMSDataType_e; // Use a function and data type to make sure switches are exhaustive -static inline CMSDataType_e CMS_DATA_TYPE(const OSD_Entry *entry) { return entry->flags & 0xF0; } +static inline CMSDataType_e CMS_DATA_TYPE(const OSD_Entry *entry) { return (CMSDataType_e)(entry->flags & 0xF0); } typedef long (*CMSMenuFuncPtr)(const OSD_Entry *from); diff --git a/src/main/io/vtx_tramp.c b/src/main/io/vtx_tramp.c index 856fc514f59..60a5b2fcb22 100644 --- a/src/main/io/vtx_tramp.c +++ b/src/main/io/vtx_tramp.c @@ -81,6 +81,7 @@ typedef struct { timeMs_t lastStatusQueryMs; int protoTimeoutCount; unsigned updateReqMask; + unsigned nextUpdateBit; // VTX capabilities struct { @@ -304,6 +305,7 @@ static void impl_Process(vtxDevice_t *vtxDevice, timeUs_t currentTimeUs) vtxState.protoTimeoutCount = 0; vtxState.request.pitModeRetries = 0; vtxState.updateReqMask = VTX_UPDATE_REQ_NONE; + vtxState.nextUpdateBit = VTX_UPDATE_REQ_FREQUENCY; vtxProtoSetState(VTX_STATE_OFFILE); break; @@ -335,21 +337,28 @@ static void impl_Process(vtxDevice_t *vtxDevice, timeUs_t currentTimeUs) // Send requests to update freqnecy and power, periodically poll device for liveness case VTX_STATE_IDLE: if (vtxState.updateReqMask != VTX_UPDATE_REQ_NONE) { - // Updates pending. Send an appropriate command - if (vtxState.updateReqMask & VTX_UPDATE_REQ_FREQUENCY) { - vtxState.updateReqMask &= ~VTX_UPDATE_REQ_FREQUENCY; + // Rotate through pending updates so a rejected value cannot + // starve another setting, including a request to leave pit mode. + unsigned updateBit = VTX_UPDATE_REQ_NONE; + for (unsigned i = 0; i < 3; i++) { + const unsigned candidate = vtxState.nextUpdateBit; + vtxState.nextUpdateBit = candidate == VTX_UPDATE_REQ_PITMODE + ? VTX_UPDATE_REQ_FREQUENCY : candidate << 1; + if (vtxState.updateReqMask & candidate) { + updateBit = candidate; + break; + } + } + vtxState.updateReqMask &= ~updateBit; + if (updateBit == VTX_UPDATE_REQ_FREQUENCY) { vtxProtoSetFrequency(vtxState.request.freq); vtxProtoSetState(VTX_STATE_QUERY_DELAY); } - else if (vtxState.updateReqMask & VTX_UPDATE_REQ_POWER) { - vtxState.updateReqMask &= ~VTX_UPDATE_REQ_POWER; + else if (updateBit == VTX_UPDATE_REQ_POWER) { vtxProtoSetPower(vtxState.request.power); vtxProtoSetState(VTX_STATE_QUERY_DELAY); } - // Pit mode last: channel and power must go out even while a - // device keeps reporting a pit state we did not ask for. - else if (vtxState.updateReqMask & VTX_UPDATE_REQ_PITMODE) { - vtxState.updateReqMask &= ~VTX_UPDATE_REQ_PITMODE; + else if (updateBit == VTX_UPDATE_REQ_PITMODE) { vtxProtoSetPitMode(vtxState.request.pitMode); vtxProtoSetState(VTX_STATE_QUERY_DELAY); } @@ -390,7 +399,8 @@ static void impl_Process(vtxDevice_t *vtxDevice, timeUs_t currentTimeUs) } if (vtxState.request.pitModeRequested && vtxState.state.pitMode != vtxState.request.pitMode) { - if (vtxState.request.pitModeRetries < VTX_PITMODE_MAX_RETRIES) { + if (!(vtxState.updateReqMask & VTX_UPDATE_REQ_PITMODE) + && vtxState.request.pitModeRetries < VTX_PITMODE_MAX_RETRIES) { vtxState.request.pitModeRetries++; vtxState.updateReqMask |= VTX_UPDATE_REQ_PITMODE; } diff --git a/src/test/unit/CMakeLists.txt b/src/test/unit/CMakeLists.txt index 60cae2f742f..ea9d3151c20 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -187,6 +187,8 @@ set_property(SOURCE vtx_pit_mode_unittest.cc PROPERTY definitions USE_VTX_CONTRO set_property(SOURCE vtx_pit_mode_unittest.cc PROPERTY depends "io/vtx.c") set_property(SOURCE vtx_tramp_unittest.cc PROPERTY definitions USE_VTX_CONTROL USE_VTX_TRAMP) +set_property(SOURCE cms_vtx_unittest.cc PROPERTY definitions USE_CMS USE_VTX_CONTROL) +set_property(SOURCE cms_vtx_unittest.cc PROPERTY depends "cms/cms_menu_vtx.c" "io/vtx_string.c") set_property(SOURCE vtx_tramp_unittest.cc PROPERTY depends "io/vtx_tramp.c" "io/vtx_string.c" "common/crc.c" "common/streambuf.c") set_property(SOURCE vtx_settings_migration_unittest.cc PROPERTY depends "config/parameter_group.c") diff --git a/src/test/unit/cms_vtx_unittest.cc b/src/test/unit/cms_vtx_unittest.cc new file mode 100644 index 00000000000..dfdd2db796a --- /dev/null +++ b/src/test/unit/cms_vtx_unittest.cc @@ -0,0 +1,123 @@ +#include +#include +#include +#include "gtest/gtest.h" + +extern "C" { +#include "platform.h" +#include "cms/cms.h" +#include "cms/cms_types.h" +#include "cms/cms_menu_vtx.h" +#include "drivers/vtx_common.h" +#include "io/vtx.h" +#include "fc/runtime_config.h" + +uint32_t armingFlags; +vtxSettingsConfig_t vtxSettingsConfig_System; +static vtxDevice_t device{}; +static bool available; +static uint8_t sentPower; +vtxDevice_t *vtxCommonDevice(void) { return available ? &device : nullptr; } +bool vtxCommonGetDeviceCapability(vtxDevice_t *dev, vtxDeviceCapability_t *out) { + if (!dev) return false; + *out = dev->capability; + return true; +} +bool vtxCommonDeviceIsReady(vtxDevice_t *) { return false; } +bool vtxCommonGetPitMode(vtxDevice_t *, uint8_t *out) { *out = 0; return true; } +bool vtxCommonGetOsdInfo(vtxDevice_t *, vtxDeviceOsdInfo_t *) { return false; } +void vtxCommonSetBandAndChannel(vtxDevice_t *, uint8_t, uint8_t) {} +void vtxCommonSetPowerByIndex(vtxDevice_t *, uint8_t power) { sentPower = power; } +void vtxCommonSetPitMode(vtxDevice_t *, uint8_t) {} +void saveConfigAndNotify(void) {} +int tfp_sprintf(char *out, const char *format, ...) { + va_list args; va_start(args, format); + const int result = vsprintf(out, format, args); + va_end(args); return result; +} +} + +class CmsVtxTest : public ::testing::Test { +protected: + const OSD_Entry *powerEntry; + OSD_TAB_t *power; + static char *twoLevels[3]; + static char *fiveLevels[6]; + void SetUp() override { + memset(&vtxSettingsConfig_System, 0, sizeof(vtxSettingsConfig_System)); + memset(&device.capability, 0, sizeof(device.capability)); + available = true; armingFlags = 0; sentPower = 0; + device.capability.powerCount = 2; + device.capability.powerNames = twoLevels; + for (const OSD_Entry *entry = cmsx_menuVtxControl.entries; entry->type != OME_BACK_AND_END; entry++) { + if (!strcmp(entry->text, "POWER")) { + powerEntry = entry; + power = (OSD_TAB_t *)entry->data; + } + } + } + void enter(uint8_t index) { + vtxSettingsConfig_System.power = index; + cmsx_menuVtxControl.onEnter(nullptr); + } + void confirm() { + for (const OSD_Entry *entry = cmsx_menuVtxControl.entries; entry->type != OME_BACK_AND_END; entry++) { + if (entry->type == OME_Submenu) { + const CMS_Menu *menu = (const CMS_Menu *)entry->data; + menu->entries[1].func(nullptr, nullptr); + return; + } + } + FAIL() << "Missing confirmation menu"; + } +}; +char *CmsVtxTest::twoLevels[] = {(char *)"---", (char *)"25", (char *)"400"}; +char *CmsVtxTest::fiveLevels[] = {(char *)"---", (char *)"25", (char *)"100", (char *)"200", (char *)"400", (char *)"600"}; + +TEST_F(CmsVtxTest, SavedIndexIsClampedBeforePowerNameIsRead) { + enter(8); + ASSERT_EQ(2, *power->val); + EXPECT_STREQ("400", power->names[*power->val]); + EXPECT_EQ(8, vtxSettingsConfig_System.power); +} + +TEST_F(CmsVtxTest, OpenMenuRefreshesAfterCapabilityDetection) { + device.capability.powerCount = 5; device.capability.powerNames = fiveLevels; + enter(5); + device.capability.powerCount = 2; device.capability.powerNames = twoLevels; + cmsVtxUpdatePowerMetadata(); + ASSERT_EQ(2, power->max); ASSERT_EQ(2, *power->val); + EXPECT_STREQ("400", power->names[*power->val]); + EXPECT_TRUE(powerEntry->flags & DYNAMIC); +} + +TEST_F(CmsVtxTest, RefreshPreservesValidUnsavedSelection) { + enter(2); *power->val = 1; + cmsVtxUpdatePowerMetadata(); + EXPECT_EQ(1, *power->val); + EXPECT_EQ(2, vtxSettingsConfig_System.power); +} + +TEST_F(CmsVtxTest, MissingDeviceHasSafeUnknownPowerSelection) { + available = false; enter(8); + EXPECT_EQ(0, power->max); ASSERT_EQ(0, *power->val); + powerEntry->func(nullptr, power); + ASSERT_EQ(0, *power->val); + EXPECT_STREQ("---", power->names[*power->val]); +} + +TEST_F(CmsVtxTest, ConfirmationRefreshesBeforeSendingOrSaving) { + device.capability.powerCount = 5; device.capability.powerNames = fiveLevels; + enter(5); + device.capability.powerCount = 2; device.capability.powerNames = twoLevels; + confirm(); + EXPECT_EQ(2, sentPower); + EXPECT_EQ(2, vtxSettingsConfig_System.power); +} + +TEST_F(CmsVtxTest, MissingDeviceCannotSaveTheUnknownPowerPlaceholder) { + available = false; enter(8); + confirm(); + EXPECT_EQ(0, sentPower); + EXPECT_EQ(8, vtxSettingsConfig_System.power); +} diff --git a/src/test/unit/vtx_tramp_unittest.cc b/src/test/unit/vtx_tramp_unittest.cc index bde406064fd..fdc5505c5b7 100644 --- a/src/test/unit/vtx_tramp_unittest.cc +++ b/src/test/unit/vtx_tramp_unittest.cc @@ -47,6 +47,7 @@ static std::vector> sent; static timeMs_t now; static uint16_t reportedMax, actualPower, actualFrequency; static bool connected; +static bool ignoreFrequency, ignorePower; static bool actualPit, ignoreNextPit, ignoreAllPit, corruptNextStatus; static void respond(char command, uint16_t a, uint16_t b, uint16_t c) @@ -87,8 +88,8 @@ void serialWriteBuf(serialPort_t *, const uint8_t *data, int size) switch (data[1]) { case 'r': respond('r', 5000, 5999, reportedMax); break; case 'v': respond('v', actualFrequency, actualPower, 0); break; - case 'F': actualFrequency = value; break; - case 'P': actualPower = value; break; + case 'F': if (!ignoreFrequency) actualFrequency = value; break; + case 'P': if (!ignorePower) actualPower = value; break; case 'I': if (ignoreAllPit) break; if (ignoreNextPit) ignoreNextPit = false; @@ -104,6 +105,7 @@ class TrampTest : public ::testing::Test { std::memset(&vtxSettingsConfig_System, 0, sizeof(vtxSettingsConfig_System)); std::memset(&vtxConfig_System, 0, sizeof(vtxConfig_System)); armingFlags = 0; connected = true; + ignoreFrequency = false; ignorePower = false; now = 0; reportedMax = 2500; actualPower = 25; actualFrequency = 5732; actualPit = false; ignoreNextPit = false; ignoreAllPit = false; corruptNextStatus = false; received.clear(); sent.clear(); device = nullptr; @@ -163,6 +165,22 @@ TEST_F(TrampTest, CustomPowerStillRespectsReportedLimit) { EXPECT_STREQ("400",device->capability.powerNames[2]); device->vTable->setPowerByIndex(device, 2); tick(20); EXPECT_EQ(400, actualPower); } +TEST_F(TrampTest, RejectedFrequencyAndPowerCannotStarvePitRequests) { + custom(); start(); + ignoreFrequency = true; ignorePower = true; + device->vTable->setBandAndChannel(device, 5, 4); + device->vTable->setPowerByIndex(device, 4); + device->vTable->setPitMode(device, 1); + tick(100); + EXPECT_GT(commands('F'), 0); + EXPECT_GT(commands('P'), 0); + EXPECT_EQ(1, commands('I', 0)); + EXPECT_TRUE(actualPit); + device->vTable->setPitMode(device, 0); + tick(100); + EXPECT_EQ(1, commands('I', 1)); + EXPECT_FALSE(actualPit); +} TEST_F(TrampTest, ExplicitOverrideAllowsConfiguredMaximum) { custom(); reportedMax = 400; vtxSettingsConfig_System.maxPowerOverride = 2500; start(); device->vTable->setPowerByIndex(device, 4); tick(20); EXPECT_EQ(2500, actualPower); From 4865f059aca15ceb2f913a61353f6ceb2611022c Mon Sep 17 00:00:00 2001 From: Raphael Hunziker Date: Wed, 23 Sep 2026 11:09:18 +0200 Subject: [PATCH 7/7] Offer the VTX pit mode box after all existing boxes MSP_BOXIDS and the activity bitmask follow the order of the active box list. A configurator that does not know VTX PIT MODE drops it from its mode list and maps the remaining activity bits by position, so a box in the middle shifted the active indicator of every later mode by one (CAMERA 1-3, USER1-4, TURTLE, MIXER PROFILE, ...). Appending it last, as IN FLIGHT MENU and THRUST REVERSE were, keeps older configurators right. --- src/main/fc/fc_msp_box.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/fc/fc_msp_box.c b/src/main/fc/fc_msp_box.c index e83a2aa0cb2..1878fac9782 100644 --- a/src/main/fc/fc_msp_box.c +++ b/src/main/fc/fc_msp_box.c @@ -345,13 +345,6 @@ void initActiveBoxIds(void) ADD_ACTIVE_BOX(BOXFAILSAFE); -#ifdef USE_VTX_CONTROL - const vtxDevice_t *vtxDevice = vtxCommonDevice(); - if (vtxDevice && vtxDevice->capability.supportsPitMode) { - ADD_ACTIVE_BOX(BOXVTXPITMODE); - } -#endif - #if defined(USE_RCDEVICE) || defined(USE_MSP_DISPLAYPORT) ADD_ACTIVE_BOX(BOXCAMERA1); ADD_ACTIVE_BOX(BOXCAMERA2); @@ -430,6 +423,15 @@ void initActiveBoxIds(void) ADD_ACTIVE_BOX(BOXTHRUSTREVERSE); } #endif + + // Keep new boxes last. Configurators that do not know a box drop it from + // their list and map the activity bits of the remaining boxes by position. +#ifdef USE_VTX_CONTROL + const vtxDevice_t *vtxDevice = vtxCommonDevice(); + if (vtxDevice && vtxDevice->capability.supportsPitMode) { + ADD_ACTIVE_BOX(BOXVTXPITMODE); + } +#endif } #define IS_ENABLED(mask) ((mask) == 0 ? 0 : 1)