diff --git a/docs/Settings.md b/docs/Settings.md index 6d7169b7c5b..01edf83ffdb 100644 --- a/docs/Settings.md +++ b/docs/Settings.md @@ -7688,6 +7688,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..57033568644 100644 --- a/docs/VTx.md +++ b/docs/VTx.md @@ -31,3 +31,68 @@ 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 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 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. 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 + +Assign **VTX PIT MODE** in Modes (permanent mode ID 73). For example, with the +first two mode-condition slots already in use, this assigns AUX5 / channel 9: + +``` +aux 2 73 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. Retries are +bounded so an unsupported command cannot block channel or power changes, and a +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. + +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/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/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.c b/src/main/fc/fc_msp.c index d00984f2d85..ce6e20be8f9 100644 --- a/src/main/fc/fc_msp.c +++ b/src/main/fc/fc_msp.c @@ -5135,7 +5135,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 742c90f4510..1878fac9782 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" @@ -120,6 +124,7 @@ static const box_t boxes[CHECKBOX_ITEM_COUNT + 1] = { { .boxId = BOXTERRAINAGLHOLD, .boxName = "TERRAIN AGL HOLD", .permanentId = 70 }, { .boxId = BOXINFLIGHTMENU, .boxName = "IN FLIGHT MENU", .permanentId = 71 }, { .boxId = BOXTHRUSTREVERSE, .boxName = "THRUST REVERSE", .permanentId = 72 }, + { .boxId = BOXVTXPITMODE, .boxName = "VTX PIT MODE", .permanentId = 73 }, { .boxId = CHECKBOX_ITEM_COUNT, .boxName = NULL, .permanentId = 0xFF } }; @@ -418,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) @@ -473,6 +487,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 a49733bfbe3..564767fecc5 100644 --- a/src/main/fc/rc_modes.h +++ b/src/main/fc/rc_modes.h @@ -89,6 +89,7 @@ typedef enum { BOXTERRAINAGLHOLD = 61, BOXINFLIGHTMENU = 62, BOXTHRUSTREVERSE = 63, + BOXVTXPITMODE = 64, CHECKBOX_ITEM_COUNT } boxId_e; diff --git a/src/main/fc/settings.yaml b/src/main/fc/settings.yaml index f805e60d658..a54b13c668c 100644 --- a/src/main/fc/settings.yaml +++ b/src/main/fc/settings.yaml @@ -4449,6 +4449,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..798cae0d7f8 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, @@ -118,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; } @@ -130,31 +137,22 @@ 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 (!vtxDevice->capability.supportsPitMode || + !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..60a5b2fcb22 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" @@ -41,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 @@ -52,6 +54,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 @@ -74,6 +81,7 @@ typedef struct { timeMs_t lastStatusQueryMs; int protoTimeoutCount; unsigned updateReqMask; + unsigned nextUpdateBit; // VTX capabilities struct { @@ -92,6 +100,9 @@ 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 + uint8_t pitModeRetries; } request; // Actual VTX state: updated from actual VTX @@ -256,7 +267,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) @@ -279,10 +291,21 @@ 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; + vtxState.nextUpdateBit = VTX_UPDATE_REQ_FREQUENCY; vtxProtoSetState(VTX_STATE_OFFILE); break; @@ -314,23 +337,31 @@ 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_PITMODE) { - // Only disabling PIT mode supported - vtxState.updateReqMask &= ~VTX_UPDATE_REQ_PITMODE; - vtxProtoSetPitMode(0); - vtxProtoSetState(VTX_STATE_QUERY_DELAY); + // 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; + } } - else if (vtxState.updateReqMask & VTX_UPDATE_REQ_FREQUENCY) { - vtxState.updateReqMask &= ~VTX_UPDATE_REQ_FREQUENCY; + 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); } + else if (updateBit == 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 @@ -367,6 +398,17 @@ 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) { + if (!(vtxState.updateReqMask & VTX_UPDATE_REQ_PITMODE) + && 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 vtxProtoSetState(VTX_STATE_IDLE); } @@ -446,7 +488,14 @@ static void impl_SetPitMode(vtxDevice_t *vtxDevice, uint8_t onoff) { UNUSED(vtxDevice); - if (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. + 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; } } @@ -555,6 +604,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, @@ -578,6 +628,55 @@ 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]; + +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; + 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; + } + + const unsigned configuredCount = count; + count = 0; + customPowerNamePointers[0] = "---"; + 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 = customPowerLevels; + 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 +743,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 60bde216173..ea9d3151c20 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -183,6 +183,16 @@ set_property(SOURCE mavlink_unittest.cc PROPERTY extra_includes set_property(SOURCE servos_swap_roll_yaw_unittest.cc PROPERTY depends "flight/servos.c" "common/maths.c" "common/filter.c" "common/lulu.c" "programming/logic_condition.c") +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 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") +set_property(SOURCE vtx_settings_migration_unittest.cc PROPERTY definitions USE_VTX_CONTROL) function(unit_test src) get_filename_component(basename ${src} NAME) @@ -232,3 +242,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/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/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; diff --git a/src/test/unit/rcdevice_unittest.cc b/src/test/unit/rcdevice_unittest.cc index 76e86bf25be..bf30f9c7c8d 100644 --- a/src/test/unit/rcdevice_unittest.cc +++ b/src/test/unit/rcdevice_unittest.cc @@ -86,6 +86,25 @@ extern "C" { } #define MAX_RESPONSES_COUNT 10 + +TEST(RcModeBitmask, PitModeAndThrustReverseRemainIndependentAcrossWordBoundary) +{ + boxBitmask_t mask = {}; + bitArraySet(mask.bits, BOXVTXPITMODE); + rcModeUpdate(&mask); + EXPECT_TRUE(IS_RC_MODE_ACTIVE(BOXVTXPITMODE)); + EXPECT_FALSE(IS_RC_MODE_ACTIVE(BOXTHRUSTREVERSE)); + + mask = {}; + bitArraySet(mask.bits, BOXTHRUSTREVERSE); + rcModeUpdate(&mask); + EXPECT_FALSE(IS_RC_MODE_ACTIVE(BOXVTXPITMODE)); + EXPECT_TRUE(IS_RC_MODE_ACTIVE(BOXTHRUSTREVERSE)); + + mask = {}; + rcModeUpdate(&mask); +} + #define FIVE_KEY_JOYSTICK_MIN FIVE_KEY_CABLE_JOYSTICK_MIN - 1 #define FIVE_KEY_JOYSTICK_MID FIVE_KEY_CABLE_JOYSTICK_MID_START + 1 #define FIVE_KEY_JOYSTICK_MAX FIVE_KEY_CABLE_JOYSTICK_MAX + 1 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..7ea21fcac77 --- /dev/null +++ b/src/test/unit/vtx_pit_mode_unittest.cc @@ -0,0 +1,103 @@ +/* + * 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, powerRequests; +static uint8_t power; +static vtxDevType_e deviceType; +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; } +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) {} +} +class PitModeTest : public ::testing::Test { +protected: + 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); } +}; +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); +} + +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 new file mode 100644 index 00000000000..fdc5505c5b7 --- /dev/null +++ b/src/test/unit/vtx_tramp_unittest.cc @@ -0,0 +1,259 @@ +/* + * 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" +#include "fc/runtime_config.h" +uint32_t armingFlags; +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 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) +{ + 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); + if (!connected) return; + switch (data[1]) { + case 'r': respond('r', 5000, 5999, reportedMax); break; + case 'v': respond('v', actualFrequency, actualPower, 0); break; + case 'F': if (!ignoreFrequency) actualFrequency = value; break; + case 'P': if (!ignorePower) actualPower = value; break; + case 'I': + if (ignoreAllPit) break; + 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)); + 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; + } + 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, 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"}; + 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(); + 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, 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); +} +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(); 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) { + 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); +} + +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); +}