Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions js/servoMixerTargetWarning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

// A servo target can't be higher than the number of servo mixer rules that
// exist (1-based: N rules means valid targets are #1..#N). Returns details
// for the warning, or null if `enteredTarget` is valid.
export function getServoTargetWarning(rules, enteredTarget) {
const ruleCount = rules.filter((rule) => rule.isUsed()).length;

if (enteredTarget <= ruleCount) {
return null;
}

return { ruleCount, enteredTarget };
}
6 changes: 6 additions & 0 deletions locale/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5268,6 +5268,12 @@
"servoMixerAdd": {
"message": "Add new mixer rule"
},
"servoMixRuleInvalidServoTargetTitle": {
"message": "Invalid servo target"
},
"servoMixRuleInvalidServoTarget": {
"message": "$1 servos are defined. Cannot output to servo #$2 because only $1 servos exist."
},
"platformType": {
"message": "Platform type"
},
Expand Down
5 changes: 5 additions & 0 deletions tabs/mixer.html
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ <h1 class="modal__title modal__title--warning" data-i18n="presetsApplyHeader"></
<a id="execute-button" class="modal__button modal__button--main" data-i18n="mixerButtonSaveAndReboot"></a>
</div>
</div>
<div id="servoTargetWarningContent" class="is-hidden">
<div class="modal__content">
<div class="modal__text"><p class="servo-target-warning-text"></p></div>
</div>
</div>
<div id="mixerWizardContent" class="is-hidden">
<div class="modal__content">
<h1 class="modal__title modal__title--warning" data-i18n="mixerWizardModalTitle"></h1>
Expand Down
24 changes: 22 additions & 2 deletions tabs/mixer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Settings from './../js/settings';
import jBox from 'jbox';
import interval from './../js/intervals';
import ServoMixRule from './../js/servoMixRule';
import { getServoTargetWarning } from './../js/servoMixerTargetWarning';
import MotorMixRule from './../js/motorMixRule';
import BitHelper from './../js/bitHelper';

Expand All @@ -29,7 +30,8 @@ mixerTab.initialize = function (callback, scrollPosition) {
$motorMixTable,
$motorMixTableBody,
modal,
motorWizardModal;
motorWizardModal,
servoTargetWarningModal;

if (GUI.active_tab !== this) {
GUI.active_tab = this;
Expand Down Expand Up @@ -380,7 +382,16 @@ mixerTab.initialize = function (callback, scrollPosition) {
});

$row.find(".mix-rule-servo").val(servoRule.getTarget()).on('change', function () {
servoRule.setTarget(Number($(this).val()));
const enteredTarget = Number($(this).val());

servoRule.setTarget(enteredTarget);

const warning = getServoTargetWarning(FC.SERVO_RULES.get(), enteredTarget);
if (warning) {
$('#servoTargetWarningContent .servo-target-warning-text')
.html(i18n.getMessage('servoMixRuleInvalidServoTarget', [warning.ruleCount, warning.enteredTarget]));
servoTargetWarningModal.open();
}
});

$row.find(".mix-rule-rate").val(servoRule.getRate()).on('change', function () {
Expand Down Expand Up @@ -972,6 +983,15 @@ mixerTab.initialize = function (callback, scrollPosition) {
content: $('#mixerApplyContent')
});

servoTargetWarningModal = new jBox('Modal', {
width: 480,
height: 200,
closeButton: 'title',
animation: false,
title: i18n.getMessage("servoMixRuleInvalidServoTargetTitle"),
content: $('#servoTargetWarningContent')
});

$('#execute-button').on('click', function () {
loadedMixerPresetID = currentMixerPreset.id;
mixer.loadServoRules(FC, currentMixerPreset);
Expand Down
91 changes: 91 additions & 0 deletions tests/servo-mixer-target-validation.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env node
/**
* Tests for the servo-mixer target validation warning (Mixer tab).
*
* A servo mixer rule's "target" is the servo's 1-based index. A target can't
* be higher than the number of servo mixer rules that exist: with N rules
* (rate != 0, i.e. isUsed()), only servos #1..#N can be validly targeted.
* Entering anything higher means some servo number in 1..N would be skipped.
*
* getServoTargetWarning() is the exact function used by tabs/mixer.js's
* .mix-rule-servo change handler.
*/

import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import ServoMixRule from '../js/servoMixRule.js';
import { getServoTargetWarning } from '../js/servoMixerTargetWarning.js';

describe('editing one of 2 existing rules (targets #1 and #2)', () => {
const makeRules = () => [ServoMixRule(1, 0, 100, 0), ServoMixRule(2, 0, 100, 0)];

test('entered 1 → no warning', () => {
assert.equal(getServoTargetWarning(makeRules(), 1), null);
});

test('entered 2 (no change) → no warning', () => {
assert.equal(getServoTargetWarning(makeRules(), 2), null);
});

test('entered 3 → warning with ruleCount=2 (only 2 rules exist, can\'t have a 3rd servo)', () => {
const warning = getServoTargetWarning(makeRules(), 3);
assert.deepEqual(warning, { ruleCount: 2, enteredTarget: 3 });
});

test('entered 4 → warning with ruleCount=2', () => {
const warning = getServoTargetWarning(makeRules(), 4);
assert.deepEqual(warning, { ruleCount: 2, enteredTarget: 4 });
});
});

describe('"Add new mixer rule" row (rate=100, auto-target via getNextUnusedIndex)', () => {
// 2 rules (#1, #2) already exist; "Add new mixer rule" adds a 3rd rule
// (target=3, rate=100, used). Now 3 rules exist, so servos #1..#3 are valid.
const makeRules = () => [
ServoMixRule(1, 0, 100, 0),
ServoMixRule(2, 0, 100, 0),
ServoMixRule(3, 0, 100, 0),
];

test('entered 1..3 → no warning', () => {
for (const entered of [1, 2, 3]) {
assert.equal(getServoTargetWarning(makeRules(), entered), null, `entered=${entered}`);
}
});

test('entered 4 → warning with ruleCount=3', () => {
const warning = getServoTargetWarning(makeRules(), 4);
assert.deepEqual(warning, { ruleCount: 3, enteredTarget: 4 });
});
});

describe('a single rule', () => {
test('entered 1 → no warning', () => {
assert.equal(getServoTargetWarning([ServoMixRule(1, 0, 100, 0)], 1), null);
});

test('entered 2 → warning with ruleCount=1 (only 1 rule exists)', () => {
const warning = getServoTargetWarning([ServoMixRule(1, 0, 100, 0)], 2);
assert.deepEqual(warning, { ruleCount: 1, enteredTarget: 2 });
});
});

describe('unused (rate=0) rules do not count toward the rule count', () => {
test('1 used + 1 unused rule: ruleCount=1, entering 2 warns', () => {
const rules = [ServoMixRule(1, 0, 100, 0), ServoMixRule(2, 0, 0, 0)];
const warning = getServoTargetWarning(rules, 2);
assert.deepEqual(warning, { ruleCount: 1, enteredTarget: 2 });
});

test('counts duplicate-target rules separately (e.g. elevon roll+pitch both targeting servo #1)', () => {
const rules = [ServoMixRule(1, 0, 100, 0), ServoMixRule(1, 0, 100, 0)];
assert.equal(getServoTargetWarning(rules, 2), null);
});
});

describe('entered = 0', () => {
test('never warns, regardless of rule count (0 means "no output")', () => {
assert.equal(getServoTargetWarning([], 0), null);
assert.equal(getServoTargetWarning([ServoMixRule(1, 0, 100, 0)], 0), null);
});
});
Loading