test(radio): enshrine rc-soar.com documented mixer/LS behavior (#7751)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a9f89323a9
commit
676721fe8f
@@ -75,10 +75,8 @@ TEST_F(SpecialFunctionsTest, FlightReset)
|
||||
#if defined(GVARS)
|
||||
TEST_F(SpecialFunctionsTest, GvarsInc)
|
||||
{
|
||||
int sw;
|
||||
for (sw = 0; sw < switchGetMaxAllSwitches(); sw += 1)
|
||||
if (g_model.getSwitchType(sw) == SWITCH_3POS)
|
||||
break;
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return;
|
||||
int swPos = (sw * 3) + SWSRC_FIRST_SWITCH;
|
||||
|
||||
simuSetSwitch(sw, 0); // SA-
|
||||
|
||||
@@ -101,6 +101,23 @@ inline void MIXER_RESET()
|
||||
logicalSwitchesReset();
|
||||
}
|
||||
|
||||
// Find a hardware switch matching the given type (SWITCH_3POS, SWITCH_2POS,
|
||||
// etc.), skipping function switches. Pass SWITCH_NONE to match any type.
|
||||
// Pass startAfter to find a second (or further) distinct switch, continuing
|
||||
// the scan after that index. Returns the switch index, or -1 if none found.
|
||||
inline int findHwSwitch(int type = SWITCH_NONE, int startAfter = -1)
|
||||
{
|
||||
for (int sw = startAfter + 1; sw < switchGetMaxAllSwitches(); sw++) {
|
||||
auto swType = g_model.getSwitchType(sw);
|
||||
if (swType == SWITCH_NONE) continue;
|
||||
#if defined(FUNCTION_SWITCHES)
|
||||
if (switchIsCustomSwitch(sw)) continue;
|
||||
#endif
|
||||
if (type == SWITCH_NONE || swType == type) return sw;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline void TELEMETRY_RESET()
|
||||
{
|
||||
telemetryData.clear();
|
||||
|
||||
+354
-25
@@ -711,11 +711,8 @@ TEST_F(MixerTest, SlowOnPhase)
|
||||
|
||||
TEST_F(MixerTest, SlowOnSwitchSource)
|
||||
{
|
||||
int sw;
|
||||
for (sw = 0; sw < switchGetMaxAllSwitches(); sw += 1)
|
||||
if (g_model.getSwitchType(sw) == SWITCH_3POS)
|
||||
break;
|
||||
if (sw >= switchGetMaxAllSwitches()) return;
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return;
|
||||
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_ADD;
|
||||
@@ -759,11 +756,8 @@ TEST_F(MixerTest, SlowOnPhasePrec10ms)
|
||||
|
||||
TEST_F(MixerTest, SlowOnSwitchSourcePrec10ms)
|
||||
{
|
||||
int sw;
|
||||
for (sw = 0; sw < switchGetMaxAllSwitches(); sw += 1)
|
||||
if (g_model.getSwitchType(sw) == SWITCH_3POS)
|
||||
break;
|
||||
if (sw >= switchGetMaxAllSwitches()) return;
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return;
|
||||
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_ADD;
|
||||
@@ -798,11 +792,8 @@ TEST_F(MixerTest, SlowDisabledOnStartup)
|
||||
|
||||
TEST_F(MixerTest, DelayOnSwitch)
|
||||
{
|
||||
int sw;
|
||||
for (sw = 0; sw < switchGetMaxAllSwitches(); sw += 1)
|
||||
if (g_model.getSwitchType(sw) == SWITCH_3POS)
|
||||
break;
|
||||
if (sw >= switchGetMaxAllSwitches()) return;
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return;
|
||||
int swPos = (sw * 3) + SWSRC_FIRST_SWITCH + 2;
|
||||
|
||||
g_model.mixData[0].destCh = 0;
|
||||
@@ -833,11 +824,8 @@ TEST_F(MixerTest, DelayOnSwitch)
|
||||
|
||||
TEST_F(MixerTest, DelayOnSwitch2)
|
||||
{
|
||||
int sw;
|
||||
for (sw = 0; sw < switchGetMaxAllSwitches(); sw += 1)
|
||||
if (g_model.getSwitchType(sw) == SWITCH_3POS)
|
||||
break;
|
||||
if (sw >= switchGetMaxAllSwitches()) return;
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return;
|
||||
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_ADD;
|
||||
@@ -1031,11 +1019,8 @@ TEST(Trainer, UnpluggedTest)
|
||||
|
||||
TEST_F(MixerTest, flightModeTransition)
|
||||
{
|
||||
int sw;
|
||||
for (sw = 0; sw < switchGetMaxAllSwitches(); sw += 1)
|
||||
if (g_model.getSwitchType(sw) == SWITCH_3POS)
|
||||
break;
|
||||
if (sw >= switchGetMaxAllSwitches()) return;
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return;
|
||||
int swPos = (sw * 3) + SWSRC_FIRST_SWITCH + 2;
|
||||
|
||||
SYSTEM_RESET();
|
||||
@@ -1249,3 +1234,347 @@ TEST_F(TrimsTest, invertedThrottlePlusThrottleTrimWithCrossTrims)
|
||||
EXPECT_EQ(channelOutputs[THR_CHAN], +1024);
|
||||
EXPECT_EQ(channelOutputs[ELE_CHAN], 0);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// rc-soar.com documented behavior tests
|
||||
// https://rc-soar.com/edgetx/index.php
|
||||
// Enshrine mixer, flight-mode, and channel-cascade semantics so that
|
||||
// refactoring does not silently break real-world user setups.
|
||||
// ==========================================================================
|
||||
|
||||
// Multiplex ADD: lines accumulate additively on the same channel.
|
||||
TEST_F(MixerTest, MultiplexAdd)
|
||||
{
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[0].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(60);
|
||||
g_model.mixData[1].destCh = 0;
|
||||
g_model.mixData[1].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[1].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[1].weight = makeSourceNumVal(40);
|
||||
|
||||
evalFlightModeMixes(e_perout_mode_normal, 0);
|
||||
EXPECT_EQ(chans[0], CHANNEL_MAX); // 60% + 40% = 100%
|
||||
}
|
||||
|
||||
// Multiplex REPL: second line replaces whatever the first produced.
|
||||
TEST_F(MixerTest, MultiplexReplace)
|
||||
{
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[0].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(100);
|
||||
g_model.mixData[1].destCh = 0;
|
||||
g_model.mixData[1].mltpx = MLTPX_REPL;
|
||||
g_model.mixData[1].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[1].weight = makeSourceNumVal(-50);
|
||||
|
||||
evalFlightModeMixes(e_perout_mode_normal, 0);
|
||||
EXPECT_EQ(chans[0], -CHANNEL_MAX / 2); // REPL overwrites to -50%
|
||||
}
|
||||
|
||||
// Multiplex MUL: multiplies with the result of all lines above.
|
||||
TEST_F(MixerTest, MultiplexMultiplyBasic)
|
||||
{
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[0].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(100);
|
||||
g_model.mixData[1].destCh = 0;
|
||||
g_model.mixData[1].mltpx = MLTPX_MUL;
|
||||
g_model.mixData[1].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[1].weight = makeSourceNumVal(50);
|
||||
|
||||
evalFlightModeMixes(e_perout_mode_normal, 0);
|
||||
EXPECT_EQ(chans[0], CHANNEL_MAX / 2); // 100% * 50% = 50%
|
||||
}
|
||||
|
||||
// Multiplex MUL is order-sensitive: ADD+ADD+MUL differs from ADD+MUL+ADD.
|
||||
TEST_F(MixerTest, MultiplexMultiplyOrderSensitive)
|
||||
{
|
||||
// Case A: ADD(60) + ADD(40) + MUL(50) = (60+40)*50% = 50%
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[0].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(60);
|
||||
g_model.mixData[1].destCh = 0;
|
||||
g_model.mixData[1].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[1].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[1].weight = makeSourceNumVal(40);
|
||||
g_model.mixData[2].destCh = 0;
|
||||
g_model.mixData[2].mltpx = MLTPX_MUL;
|
||||
g_model.mixData[2].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[2].weight = makeSourceNumVal(50);
|
||||
|
||||
evalFlightModeMixes(e_perout_mode_normal, 0);
|
||||
int32_t caseA = chans[0];
|
||||
EXPECT_EQ(caseA, CHANNEL_MAX / 2); // 100% * 50% = 50%
|
||||
|
||||
// Case B: ADD(60) + MUL(50) + ADD(40) = 60*50% + 40 = 30+40 = 70%
|
||||
MIXER_RESET();
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[0].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(60);
|
||||
g_model.mixData[1].destCh = 0;
|
||||
g_model.mixData[1].mltpx = MLTPX_MUL;
|
||||
g_model.mixData[1].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[1].weight = makeSourceNumVal(50);
|
||||
g_model.mixData[2].destCh = 0;
|
||||
g_model.mixData[2].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[2].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[2].weight = makeSourceNumVal(40);
|
||||
|
||||
evalFlightModeMixes(e_perout_mode_normal, 0);
|
||||
int32_t caseB = chans[0];
|
||||
|
||||
EXPECT_NE(caseA, caseB); // order matters
|
||||
}
|
||||
|
||||
// Weight-then-offset: output = (source * weight) + offset.
|
||||
TEST_F(MixerTest, WeightThenOffset)
|
||||
{
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].srcRaw = MIXSRC_FIRST_STICK;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(50);
|
||||
g_model.mixData[0].offset = makeSourceNumVal(50);
|
||||
|
||||
// Stick at +100%: 50% of 1024 + 50% offset
|
||||
anaSetFiltered(inputMappingConvertMode(0), +1024);
|
||||
evalFlightModeMixes(e_perout_mode_normal, 0);
|
||||
int32_t full = chans[0];
|
||||
|
||||
// Stick at 0: 0 + 50% offset
|
||||
anaSetFiltered(inputMappingConvertMode(0), 0);
|
||||
evalFlightModeMixes(e_perout_mode_normal, 0);
|
||||
int32_t mid = chans[0];
|
||||
|
||||
// Stick at -100%: -50% + 50% offset = 0
|
||||
anaSetFiltered(inputMappingConvertMode(0), -1024);
|
||||
evalFlightModeMixes(e_perout_mode_normal, 0);
|
||||
int32_t low = chans[0];
|
||||
|
||||
// full should be ~100%, mid ~50%, low ~0%
|
||||
EXPECT_NEAR(full, CHANNEL_MAX, CHANNEL_MAX / 100);
|
||||
EXPECT_NEAR(mid, CHANNEL_MAX / 2, CHANNEL_MAX / 100);
|
||||
EXPECT_NEAR(low, 0, CHANNEL_MAX / 100);
|
||||
}
|
||||
|
||||
// Cascaded channels bypass output clipping: a channel used as a mix source
|
||||
// carries its internal (>100%) value, not the clipped output.
|
||||
TEST_F(MixerTest, CascadedChannelBypassesOutputClipping)
|
||||
{
|
||||
// CH0: stick at 200% weight (overdrives to 200% internally)
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].srcRaw = MIXSRC_FIRST_STICK;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(200);
|
||||
// CH1: reads CH0 at 50% weight — if unclipped, 200%*50% = 100%
|
||||
g_model.mixData[1].destCh = 1;
|
||||
g_model.mixData[1].srcRaw = MIXSRC_FIRST_CH;
|
||||
g_model.mixData[1].weight = makeSourceNumVal(50);
|
||||
|
||||
anaSetFiltered(inputMappingConvertMode(0), +1024);
|
||||
evalMixes(1);
|
||||
|
||||
// CH0 output is clipped to 100%
|
||||
EXPECT_EQ(channelOutputs[0], 1024);
|
||||
// CH1 sees the unclipped CH0 (200%), applies 50% → 100%
|
||||
// If CH0 were clipped before cascading, CH1 would be only 50%.
|
||||
EXPECT_GT(channelOutputs[1], 512); // must be more than 50%
|
||||
EXPECT_NEAR(channelOutputs[1], 1024, 2); // should be ~100%
|
||||
}
|
||||
|
||||
// Flight mode priority: FM1 has highest priority, FM0 is fallback.
|
||||
// getFlightMode() iterates FM1..FM8, first match wins; FM0 returned if none.
|
||||
TEST_F(MixerTest, FlightModePriority)
|
||||
{
|
||||
// Find two distinct 3-pos switches
|
||||
int sw1 = findHwSwitch(SWITCH_3POS);
|
||||
int sw2 = findHwSwitch(SWITCH_3POS, sw1);
|
||||
if (sw2 < 0) return; // not enough switches on this target
|
||||
|
||||
int sw1Up = (sw1 * 3) + SWSRC_FIRST_SWITCH;
|
||||
int sw2Up = (sw2 * 3) + SWSRC_FIRST_SWITCH;
|
||||
|
||||
// FM1 activated by sw1↑, FM2 by sw2↑
|
||||
g_model.flightModeData[1].swtch = sw1Up;
|
||||
g_model.flightModeData[2].swtch = sw2Up;
|
||||
|
||||
// Three REPL mix lines, one per FM, using distinguishable weights:
|
||||
// FM0-only: weight=10 FM1-only: weight=20 FM2-only: weight=30
|
||||
// flightModes bitmask: bit set = DISABLED in that FM.
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_REPL;
|
||||
g_model.mixData[0].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(10);
|
||||
g_model.mixData[0].flightModes = ~1u & 0x1FF; // enabled only in FM0
|
||||
|
||||
g_model.mixData[1].destCh = 0;
|
||||
g_model.mixData[1].mltpx = MLTPX_REPL;
|
||||
g_model.mixData[1].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[1].weight = makeSourceNumVal(20);
|
||||
g_model.mixData[1].flightModes = ~2u & 0x1FF; // enabled only in FM1
|
||||
|
||||
g_model.mixData[2].destCh = 0;
|
||||
g_model.mixData[2].mltpx = MLTPX_REPL;
|
||||
g_model.mixData[2].srcRaw = MIXSRC_MAX;
|
||||
g_model.mixData[2].weight = makeSourceNumVal(30);
|
||||
g_model.mixData[2].flightModes = ~4u & 0x1FF; // enabled only in FM2
|
||||
|
||||
// Neither switch active → FM0 (fallback)
|
||||
simuSetSwitch(sw1, 0);
|
||||
simuSetSwitch(sw2, 0);
|
||||
evalMixes(1);
|
||||
EXPECT_NEAR(channelOutputs[0], 1024 * 10 / 100, 1);
|
||||
|
||||
// Both switches active → FM1 wins (higher priority than FM2)
|
||||
simuSetSwitch(sw1, -1);
|
||||
simuSetSwitch(sw2, -1);
|
||||
evalMixes(1);
|
||||
EXPECT_NEAR(channelOutputs[0], 1024 * 20 / 100, 1);
|
||||
|
||||
// Only sw2 active → FM2
|
||||
simuSetSwitch(sw1, 0);
|
||||
simuSetSwitch(sw2, -1);
|
||||
evalMixes(1);
|
||||
EXPECT_NEAR(channelOutputs[0], 1024 * 30 / 100, 1);
|
||||
}
|
||||
|
||||
// Cumulative weight through cascaded channels:
|
||||
// CH0 = stick * 80%, CH1 = CH0 * 25% → CH1 should be stick * 20%.
|
||||
TEST_F(MixerTest, CascadedWeightMultiplication)
|
||||
{
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].srcRaw = MIXSRC_FIRST_STICK;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(80);
|
||||
g_model.mixData[1].destCh = 1;
|
||||
g_model.mixData[1].srcRaw = MIXSRC_FIRST_CH;
|
||||
g_model.mixData[1].weight = makeSourceNumVal(25);
|
||||
|
||||
anaSetFiltered(inputMappingConvertMode(0), +1024);
|
||||
evalMixes(1);
|
||||
|
||||
// CH0 = 80% of 1024 ≈ 819
|
||||
EXPECT_NEAR(channelOutputs[0], 1024 * 80 / 100, 2);
|
||||
// CH1 = 25% of CH0 = 20% of 1024 ≈ 205
|
||||
EXPECT_NEAR(channelOutputs[1], 1024 * 20 / 100, 2);
|
||||
}
|
||||
|
||||
// Sequencer pattern (rc-soar.com/edgetx/setups/sequencer):
|
||||
// CH0 = timebase driven by a switch with slow up/down (linear ramp).
|
||||
// CH1 = servo channel reading CH0 through a custom curve.
|
||||
// A flat curve segment produces a "pause" where the servo holds position
|
||||
// while the timebase continues to ramp.
|
||||
TEST_F(MixerTest, SequencerSlowRampThroughCurve)
|
||||
{
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return;
|
||||
|
||||
// --- Curve setup: 5-point standard curve on slot 0 ---
|
||||
// Points at x = -100, -50, 0, +50, +100 (standard = evenly spaced)
|
||||
// y-values: -100, -100, 0, 0, +100
|
||||
// Segment 1 (-100..-50): flat at -100 (pause)
|
||||
// Segment 2 (-50..0): ramp from -100 to 0
|
||||
// Segment 3 (0..+50): flat at 0 (pause)
|
||||
// Segment 4 (+50..+100): ramp from 0 to +100
|
||||
g_model.curves[0].type = CURVE_TYPE_STANDARD;
|
||||
g_model.curves[0].smooth = 0; // linear interpolation, no smoothing
|
||||
g_model.curves[0].points = 0; // 0 means 5 points (default)
|
||||
int8_t *pts = curveAddress(0);
|
||||
pts[0] = -100; // x=-100%
|
||||
pts[1] = -100; // x=-50% (flat: pause)
|
||||
pts[2] = 0; // x=0%
|
||||
pts[3] = 0; // x=+50% (flat: pause)
|
||||
pts[4] = 100; // x=+100%
|
||||
|
||||
// --- CH0: timebase — switch source with slow ---
|
||||
// speedUp=10 → 1.0s full traversal (-100% to +100%), 100 ticks
|
||||
g_model.mixData[0].destCh = 0;
|
||||
g_model.mixData[0].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[0].srcRaw = sw + MIXSRC_FIRST_SWITCH;
|
||||
g_model.mixData[0].weight = makeSourceNumVal(100);
|
||||
g_model.mixData[0].speedUp = 10; // 1.0s
|
||||
g_model.mixData[0].speedDown = 10; // 1.0s
|
||||
|
||||
// --- CH1: servo — reads CH0 through curve ---
|
||||
g_model.mixData[1].destCh = 1;
|
||||
g_model.mixData[1].mltpx = MLTPX_ADD;
|
||||
g_model.mixData[1].srcRaw = MIXSRC_FIRST_CH;
|
||||
g_model.mixData[1].weight = makeSourceNumVal(100);
|
||||
g_model.mixData[1].curve.type = CURVE_REF_CUSTOM;
|
||||
g_model.mixData[1].curve.value = makeSourceNumVal(1); // curve index 0 (1-based)
|
||||
|
||||
s_mixer_first_run_done = true;
|
||||
|
||||
// Start: switch up → CH0 targets -100%
|
||||
simuSetSwitch(sw, -1);
|
||||
for (int i = 0; i < 110; i++)
|
||||
evalFlightModeMixes(e_perout_mode_normal, 1);
|
||||
evalMixes(1); // run limits to populate channelOutputs
|
||||
|
||||
int16_t timebaseStart = channelOutputs[0];
|
||||
int16_t servoStart = channelOutputs[1];
|
||||
// Timebase at -100%, curve maps -100→-100
|
||||
EXPECT_NEAR(timebaseStart, -1024, 20);
|
||||
EXPECT_NEAR(servoStart, -1024, 20);
|
||||
|
||||
// Flip switch down → ramp begins from -100% toward +100%
|
||||
simuSetSwitch(sw, 1);
|
||||
|
||||
// Advance 25 ticks (25% of 1.0s = timebase at ~-50%)
|
||||
// Curve: flat from -100% to -50%, so servo should still be near -100%
|
||||
for (int i = 0; i < 25; i++)
|
||||
evalFlightModeMixes(e_perout_mode_normal, 1);
|
||||
evalMixes(1);
|
||||
|
||||
int16_t timebaseQ1 = channelOutputs[0];
|
||||
int16_t servoQ1 = channelOutputs[1];
|
||||
EXPECT_GT(timebaseQ1, timebaseStart + 100) << "Timebase should have moved";
|
||||
EXPECT_NEAR(servoQ1, -1024, 60) << "Servo should hold during flat segment (pause)";
|
||||
|
||||
// Advance to 50 ticks total (50% = timebase at ~0%)
|
||||
// Curve: ramp from -100 to 0, so servo should be near 0%
|
||||
for (int i = 0; i < 25; i++)
|
||||
evalFlightModeMixes(e_perout_mode_normal, 1);
|
||||
evalMixes(1);
|
||||
|
||||
int16_t timebaseMid = channelOutputs[0];
|
||||
int16_t servoMid = channelOutputs[1];
|
||||
EXPECT_NEAR(timebaseMid, 0, 60);
|
||||
EXPECT_NEAR(servoMid, 0, 60);
|
||||
|
||||
// Advance to 75 ticks (75% = timebase at ~+50%)
|
||||
// Curve: flat from 0 to +50%, so servo should hold near 0%
|
||||
for (int i = 0; i < 25; i++)
|
||||
evalFlightModeMixes(e_perout_mode_normal, 1);
|
||||
evalMixes(1);
|
||||
|
||||
int16_t timebaseQ3 = channelOutputs[0];
|
||||
int16_t servoQ3 = channelOutputs[1];
|
||||
EXPECT_GT(timebaseQ3, timebaseMid + 100) << "Timebase should have advanced";
|
||||
// Servo should be near the flat segment value (0), but may slightly
|
||||
// overshoot into the final ramp segment due to discrete stepping.
|
||||
EXPECT_NEAR(servoQ3, 0, 150) << "Servo should be near flat segment";
|
||||
EXPECT_LT(servoQ3, 512) << "Servo must not yet reach the final ramp";
|
||||
|
||||
// Advance to ~100 ticks (100% = timebase at +100%)
|
||||
// Curve: ramp from 0 to +100, so servo should be near +100%
|
||||
for (int i = 0; i < 30; i++)
|
||||
evalFlightModeMixes(e_perout_mode_normal, 1);
|
||||
evalMixes(1);
|
||||
|
||||
int16_t timebaseEnd = channelOutputs[0];
|
||||
int16_t servoEnd = channelOutputs[1];
|
||||
EXPECT_NEAR(timebaseEnd, 1024, 20);
|
||||
EXPECT_NEAR(servoEnd, 1024, 40);
|
||||
|
||||
// Reverse: flip switch up — sequence should reverse automatically
|
||||
simuSetSwitch(sw, -1);
|
||||
for (int i = 0; i < 110; i++)
|
||||
evalFlightModeMixes(e_perout_mode_normal, 1);
|
||||
evalMixes(1);
|
||||
|
||||
EXPECT_NEAR(channelOutputs[0], -1024, 20) << "Timebase should return to -100%";
|
||||
EXPECT_NEAR(channelOutputs[1], -1024, 40) << "Servo should return to start via reversed curve";
|
||||
}
|
||||
|
||||
@@ -40,17 +40,14 @@ void setLogicalSwitch(int index, uint16_t _func, int16_t _v1, int16_t _v2, int16
|
||||
#define SWSRC_SW1 (SWSRC_FIRST_LOGICAL_SWITCH)
|
||||
#define SWSRC_SW2 (SWSRC_FIRST_LOGICAL_SWITCH + 1)
|
||||
|
||||
#if defined(PCBTARANIS)
|
||||
TEST(getSwitch, OldTypeStickyCSW)
|
||||
{
|
||||
RADIO_RESET();
|
||||
MODEL_RESET();
|
||||
MIXER_RESET();
|
||||
|
||||
int sw;
|
||||
for (sw = 0; sw < switchGetMaxAllSwitches(); sw += 1)
|
||||
if (g_model.getSwitchType(sw) == SWITCH_3POS)
|
||||
break;
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return; // no 3-pos switch on this target
|
||||
int swPos = (sw * 3) + SWSRC_FIRST_SWITCH;
|
||||
|
||||
setLogicalSwitch(0, LS_FUNC_AND, swPos, SWSRC_NONE);
|
||||
@@ -79,7 +76,76 @@ TEST(getSwitch, OldTypeStickyCSW)
|
||||
EXPECT_FALSE(getSwitch(SWSRC_SW1));
|
||||
EXPECT_FALSE(getSwitch(SWSRC_SW2));
|
||||
}
|
||||
#endif
|
||||
|
||||
// Tick logical switches for N cycles.
|
||||
static void tickLogicalSwitches(int n)
|
||||
{
|
||||
for (int i = 0; i < n; i++) {
|
||||
logicalSwitchesTimerTick();
|
||||
evalLogicalSwitches();
|
||||
}
|
||||
}
|
||||
|
||||
// Sticky LS latches at startup when the set-condition is already satisfied.
|
||||
// After logicalSwitchesReset(), lastValue starts at its CS_LAST_VALUE_INIT
|
||||
// sentinel, so the first logicalSwitchesTimerTick() sees a 0->1 edge on the
|
||||
// set-condition and latches immediately.
|
||||
TEST(getSwitch, StickyLatchesAtStartupWhenSetConditionTrue)
|
||||
{
|
||||
RADIO_RESET();
|
||||
MODEL_RESET();
|
||||
MIXER_RESET();
|
||||
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return; // no 3-pos switch on this target
|
||||
int swUp = (sw * 3) + SWSRC_FIRST_SWITCH;
|
||||
int swDn = swUp + 2;
|
||||
|
||||
// Pre-set the switch to the "set" position BEFORE creating the LS
|
||||
simuSetSwitch(sw, -1);
|
||||
|
||||
setLogicalSwitch(0, LS_FUNC_STICKY, swUp, swDn);
|
||||
|
||||
// Simulate power-on
|
||||
logicalSwitchesReset();
|
||||
|
||||
// After reset + tick, the set-condition is already true so sticky latches
|
||||
tickLogicalSwitches(10);
|
||||
EXPECT_TRUE(getSwitch(SWSRC_SW1))
|
||||
<< "Sticky latches at startup when set-condition is already true";
|
||||
|
||||
// Reset via the reset-condition (swDn)
|
||||
simuSetSwitch(sw, 1);
|
||||
tickLogicalSwitches(5);
|
||||
EXPECT_FALSE(getSwitch(SWSRC_SW1)) << "Reset condition should unlatch sticky";
|
||||
}
|
||||
|
||||
// Sticky LS stays false at startup when the set-condition is NOT satisfied.
|
||||
TEST(getSwitch, StickyFalseAtStartupWhenSetConditionFalse)
|
||||
{
|
||||
RADIO_RESET();
|
||||
MODEL_RESET();
|
||||
MIXER_RESET();
|
||||
|
||||
int sw = findHwSwitch(SWITCH_3POS);
|
||||
if (sw < 0) return; // no 3-pos switch on this target
|
||||
int swUp = (sw * 3) + SWSRC_FIRST_SWITCH;
|
||||
int swDn = swUp + 2;
|
||||
|
||||
// Switch in mid position - set-condition is false
|
||||
simuSetSwitch(sw, 0);
|
||||
|
||||
setLogicalSwitch(0, LS_FUNC_STICKY, swUp, swDn);
|
||||
|
||||
logicalSwitchesReset();
|
||||
tickLogicalSwitches(10);
|
||||
EXPECT_FALSE(getSwitch(SWSRC_SW1)) << "Sticky should not latch when condition is false";
|
||||
|
||||
// Now trigger a rising edge -> should latch
|
||||
simuSetSwitch(sw, -1);
|
||||
tickLogicalSwitches(5);
|
||||
EXPECT_TRUE(getSwitch(SWSRC_SW1)) << "Sticky should latch on rising edge";
|
||||
}
|
||||
|
||||
TEST(getSwitch, nullSW)
|
||||
{
|
||||
@@ -88,7 +154,6 @@ TEST(getSwitch, nullSW)
|
||||
}
|
||||
|
||||
|
||||
#if defined(PCBTARANIS)
|
||||
TEST(getSwitch, inputWithTrim)
|
||||
{
|
||||
MODEL_RESET();
|
||||
@@ -107,7 +172,6 @@ TEST(getSwitch, inputWithTrim)
|
||||
evalLogicalSwitches();
|
||||
EXPECT_TRUE(getSwitch(SWSRC_SW1));
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(evalLogicalSwitches, playFile)
|
||||
{
|
||||
@@ -143,13 +207,9 @@ TEST(evalLogicalSwitches, playFile)
|
||||
|
||||
TEST(getSwitch, edgeInstant)
|
||||
{
|
||||
int sw1, sw2;
|
||||
for (sw1 = 0; sw1 < switchGetMaxAllSwitches(); sw1 += 1)
|
||||
if (g_model.getSwitchType(sw1) == SWITCH_3POS)
|
||||
break;
|
||||
for (sw2 = sw1 + 1; sw2 < switchGetMaxAllSwitches(); sw2 += 1)
|
||||
if (g_model.getSwitchType(sw2) == SWITCH_3POS)
|
||||
break;
|
||||
int sw1 = findHwSwitch(SWITCH_3POS);
|
||||
int sw2 = findHwSwitch(SWITCH_3POS, sw1);
|
||||
if (sw1 < 0 || sw2 < 0) return; // needs two distinct 3-pos switches
|
||||
int sw1Pos = (sw1 * 3) + SWSRC_FIRST_SWITCH;
|
||||
int sw2Pos = (sw2 * 3) + SWSRC_FIRST_SWITCH;
|
||||
|
||||
@@ -265,13 +325,9 @@ TEST(getSwitch, edgeInstant)
|
||||
|
||||
TEST(getSwitch, edgeRelease)
|
||||
{
|
||||
int sw1, sw2;
|
||||
for (sw1 = 0; sw1 < switchGetMaxAllSwitches(); sw1 += 1)
|
||||
if (g_model.getSwitchType(sw1) == SWITCH_3POS)
|
||||
break;
|
||||
for (sw2 = sw1 + 1; sw2 < switchGetMaxAllSwitches(); sw2 += 1)
|
||||
if (g_model.getSwitchType(sw2) == SWITCH_3POS)
|
||||
break;
|
||||
int sw1 = findHwSwitch(SWITCH_3POS);
|
||||
int sw2 = findHwSwitch(SWITCH_3POS, sw1);
|
||||
if (sw1 < 0 || sw2 < 0) return; // needs two distinct 3-pos switches
|
||||
int sw1Pos = (sw1 * 3) + SWSRC_FIRST_SWITCH;
|
||||
int sw2Pos = (sw2 * 3) + SWSRC_FIRST_SWITCH;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user