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
18 changes: 18 additions & 0 deletions locale/de/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5402,6 +5402,24 @@
"mixerWizardStop": {
"message": "Not-Stopp"
},
"mixerWizardDirectionQuestion": {
"message": "dreht. Dreht er so, wie der Pfeil an seiner Position zeigt?"
},
"mixerWizardDirectionHint": {
"message": "„Umkehren“ weist den ESC per DShot-Befehl an, andersherum zu drehen; im ESC selbst wird nichts geändert. Danach den Mixer speichern, damit es erhalten bleibt."
},
"mixerWizardDirectionSpin": {
"message": "Nochmals drehen"
},
"mixerWizardDirectionCorrect": {
"message": "Dreht richtig"
},
"mixerWizardDirectionReverse": {
"message": "Umkehren"
},
"mixerWizardDirectionSkip": {
"message": "Richtungsprüfung überspringen"
},
"settings": {
"message": "Einstellungen"
},
Expand Down
18 changes: 18 additions & 0 deletions locale/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5852,6 +5852,24 @@
"mixerWizardStop": {
"message": "Emergency Stop"
},
"mixerWizardDirectionQuestion": {
"message": "is spinning. Does it turn the way the arrow at its position shows?"
},
"mixerWizardDirectionHint": {
"message": "Reverse tells that ESC to spin the other way with a DShot command; nothing is changed in the ESC itself. Save the mixer afterwards to keep it."
},
"mixerWizardDirectionSpin": {
"message": "Spin again"
},
"mixerWizardDirectionCorrect": {
"message": "Turns correctly"
},
"mixerWizardDirectionReverse": {
"message": "Reverse"
},
"mixerWizardDirectionSkip": {
"message": "Skip direction check"
},
"settings": {
"message": "Settings"
},
Expand Down
7 changes: 7 additions & 0 deletions src/css/tabs/mixer.css
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,13 @@
border-color: #1e8449;
}

/* Position button of the motor whose spin direction is being checked */
.wizard-position-btn.checking {
background-color: #f39c12;
border-color: #d68910;
animation: pulse 1s infinite;
}

@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
Expand Down
19 changes: 19 additions & 0 deletions tabs/mixer.html
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,25 @@ <h1 class="modal__title modal__title--warning" data-i18n="mixerWizardModalTitle"
</div>
</div>

<!-- Direction check (DShot only, shown once every motor has been located) -->
<div id="wizard-direction" class="wizard-section is-hidden">
<p class="wizard-prompt">
<span data-i18n="mixerWizardLocating"></span>
<strong id="wizard-direction-motor">1</strong>
<span data-i18n="mixerWizardDirectionQuestion"></span>
</p>
<p data-i18n="mixerWizardDirectionHint"></p>
<div class="wizard-buttons">
<a id="wizard-direction-spin" class="modal__button" data-i18n="mixerWizardDirectionSpin">Spin again</a>
<a id="wizard-direction-ok" class="modal__button modal__button--main" data-i18n="mixerWizardDirectionCorrect">Turns correctly</a>
<a id="wizard-direction-reverse" class="modal__button" data-i18n="mixerWizardDirectionReverse">Reverse</a>
</div>
<div class="wizard-buttons">
<a id="wizard-direction-skip" class="modal__button" data-i18n="mixerWizardDirectionSkip">Skip direction check</a>
<a id="wizard-direction-stop" class="modal__button modal__button--warning" data-i18n="mixerWizardStop">Emergency Stop</a>
</div>
</div>

<!-- Wizard complete (shown when all motors identified) -->
<div id="wizard-complete" class="wizard-section is-hidden">
<p data-i18n="mixerWizardComplete"></p>
Expand Down
115 changes: 110 additions & 5 deletions tabs/mixer.js
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,11 @@ mixerTab.initialize = function (callback, scrollPosition) {
motorPositions: {}, // Map: motorIndex -> positionIndex
locateInterval: null, // Interval for repeating locate command
isActive: false, // Is wizard in progress?
savedDshotBeeper: null // Original dshot_beeper_enabled value to restore
savedDshotBeeper: null, // Original dshot_beeper_enabled value to restore
directionAvailable: false, // DShot with a firmware that has dshot_reversed_motors
directionMotor: 0, // Which motor's spin direction is being checked
directionTimer: null, // Timeout that ends a direction spin or delays the next one
reversedMask: 0 // dshot_reversed_motors as currently set in the FC
};

function sendMotorValues(motorIndex, value) {
Expand Down Expand Up @@ -712,6 +716,7 @@ mixerTab.initialize = function (callback, scrollPosition) {

$('#wizard-intro').removeClass('is-hidden');
$('#wizard-progress').addClass('is-hidden');
$('#wizard-direction').addClass('is-hidden');
$('#wizard-complete').addClass('is-hidden');

// Regenerate progress steps for this motor count
Expand Down Expand Up @@ -803,7 +808,11 @@ mixerTab.initialize = function (callback, scrollPosition) {

if (wizardState.currentMotor >= currentMixerPreset.motorMixer.length) {
// All motors identified
wizardComplete();
if (wizardState.directionAvailable) {
startDirectionCheck();
} else {
wizardComplete();
}
} else {
// Start locating next motor
startLocatingMotor(wizardState.currentMotor);
Expand All @@ -817,9 +826,87 @@ mixerTab.initialize = function (callback, scrollPosition) {
$('.wizard-progress-step').removeClass('active').addClass('complete');

$('#wizard-progress').addClass('is-hidden');
$('#wizard-direction').addClass('is-hidden');
$('#wizard-complete').removeClass('is-hidden');
}

// Direction check, DShot only. Once the positions are known each output is spun for a
// moment and the user says whether it turns the way the arrows show. "Reverse" flips
// that output's bit in dshot_reversed_motors; the firmware sends the new direction
// commands within about 100 ms of the change, so the next spin shows the result.
// Nothing is written to the ESC itself.
const DIRECTION_SPIN_MS = 2000;
const DIRECTION_APPLY_DELAY_MS = 300;

function stopDirectionSpin() {
if (wizardState.directionTimer) {
clearTimeout(wizardState.directionTimer);
wizardState.directionTimer = null;
}
stopMotors();
}

function spinMotorForDirection(motorIndex) {
stopDirectionSpin();
if (!wizardState.isActive) return;

const spinValue = Math.round(FC.MISC.mincommand + 0.15 * (FC.MISC.maxthrottle - FC.MISC.mincommand));
sendMotorValues(motorIndex, spinValue);
wizardState.locateInterval = setInterval(function() {
sendMotorValues(motorIndex, spinValue);
}, 50);
wizardState.directionTimer = setTimeout(stopDirectionSpin, DIRECTION_SPIN_MS);
}

function showDirectionMotor(motorIndex) {
$('#wizard-direction-motor').text(motorIndex + 1);
$('.wizard-position-btn').removeClass('checking');
$(`#wizardPos${wizardState.motorPositions[motorIndex]}`).addClass('checking');
spinMotorForDirection(motorIndex);
}

function startDirectionCheck() {
wizardState.directionMotor = 0;
$('#wizard-progress').addClass('is-hidden');
$('#wizard-direction').removeClass('is-hidden');
showDirectionMotor(0);
}

function endDirectionCheck() {
stopDirectionSpin();
$('.wizard-position-btn').removeClass('checking');
wizardComplete();
}

$('#wizard-direction-spin').on('click', function() {
spinMotorForDirection(wizardState.directionMotor);
});

$('#wizard-direction-ok').on('click', function() {
stopDirectionSpin();
wizardState.directionMotor++;
if (wizardState.directionMotor >= currentMixerPreset.motorMixer.length) {
endDirectionCheck();
} else {
showDirectionMotor(wizardState.directionMotor);
}
});

$('#wizard-direction-reverse').on('click', function() {
stopDirectionSpin();
const motorIndex = wizardState.directionMotor;
wizardState.reversedMask ^= (1 << motorIndex);
mspHelper.setSetting('dshot_reversed_motors', wizardState.reversedMask, function() {
// Give the firmware time to notice the change and get the command frames out
wizardState.directionTimer = setTimeout(function() {
Comment on lines +898 to +901

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Failed reversals look successful 🐞 Bug ☼ Reliability

The Reverse handler updates reversedMask and schedules the verification spin from setSetting's
callback even when the setting write fails. A transport or encoding failure therefore leaves the
controller unchanged while the wizard continues without an error and bases later full-mask writes on
a value that was never applied.
Agent Prompt
## Issue description
The direction wizard proceeds as though a reversal succeeded even when the setting write failed, leaving its local mask inconsistent with the flight controller.

## Fix Focus Areas
- tabs/mixer.js[895-905]
- js/msp/MSPHelper.js[3802-3809]

## Recommended Fix
Use a setting-write API that exposes success or failure, and only commit `reversedMask` and schedule the verification spin after confirmed success. On failure, restore the previous mask, keep the current motor selected, show an actionable error, and leave the motor stopped.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

wizardState.directionTimer = null;
spinMotorForDirection(motorIndex);
Comment on lines +899 to +903

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. A stale reversal spins the wrong motor 🐞 Bug ☼ Reliability

The Reverse handler captures directionMotor and schedules a later spin without verifying that the
wizard is still checking the same motor and run. If the user advances, clicks Reverse repeatedly, or
closes and quickly reopens before setSetting completes, the callback can interrupt the current
spin and energize an earlier output.
Agent Prompt
## Issue description
Asynchronous reversal callbacks can outlive the motor or wizard run that created them and subsequently spin the wrong output.

## Fix Focus Areas
- tabs/mixer.js[895-905]
- tabs/mixer.js[1015-1017]

## Recommended Fix
Track a wizard-run generation and the expected direction motor when starting the setting operation. Before scheduling or starting the follow-up spin, verify that the wizard is active, the generation still matches, and `directionMotor` still equals the captured motor; also disable or serialize direction actions while a reversal is pending.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}, DIRECTION_APPLY_DELAY_MS);
});
});

$('#wizard-direction-skip').on('click', endDirectionCheck);

// Position button click handler — delegated so it works on dynamically generated buttons
$('.wizard-motor-preview').on('click', '.wizard-position-btn', function() {
if (!$(this).hasClass('waiting')) return;
Expand Down Expand Up @@ -855,8 +942,8 @@ mixerTab.initialize = function (callback, scrollPosition) {
});

// Emergency stop button click handler
$('#wizard-stop-button').on('click', function() {
stopMotors();
$('#wizard-stop-button, #wizard-direction-stop').on('click', function() {
stopDirectionSpin();
motorWizardModal.close();
});

Expand Down Expand Up @@ -904,11 +991,29 @@ mixerTab.initialize = function (callback, scrollPosition) {
}

buildPositionButtons(positions);

// The direction step needs DShot and a firmware that knows the setting; anything
// else (older firmware, PWM ESCs) just ends the wizard after the positions
wizardState.directionAvailable = false;
wizardState.reversedMask = 0;
Promise.all([
mspHelper.getSetting('motor_pwm_protocol'),
mspHelper.getSetting('dshot_reversed_motors'),
]).then(function([protocol, reversed]) {
Comment on lines +999 to +1002

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Slow reads skip direction checks 🐞 Bug ≡ Correctness

The modal initializes directionAvailable to false and updates it only after two asynchronous
setting reads, while the position-completion path immediately branches on its current value. If all
positions are selected before those reads resolve, a supported DShot controller goes directly to
completion and the later result cannot enter the omitted phase.
Agent Prompt
## Issue description
Position identification can finish before asynchronous DShot capability detection, causing a supported controller to skip the direction phase.

## Fix Focus Areas
- tabs/mixer.js[808-815]
- tabs/mixer.js[995-1011]

## Recommended Fix
Store the capability lookup Promise for the current wizard run and await it before choosing between `startDirectionCheck` and `wizardComplete`. Disable Start until detection settles or show a pending state at position completion, while preserving the fallback to completion when either setting is unavailable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

if (!protocol || !reversed || !protocol.setting.table) return;
const protocolName = protocol.setting.table.values[protocol.value] || '';
if (protocolName.startsWith('DSHOT')) {
wizardState.directionAvailable = true;
wizardState.reversedMask = reversed.value;
}
}).catch(function() {
// Setting unknown to this firmware: keep the direction step off
});
};

// Clean up when modal closes
motorWizardModal.options.onClose = function() {
stopMotors();
stopDirectionSpin();
wizardState.isActive = false;

// Restore DShot beeper if it was enabled before the wizard
Expand Down
Loading